← Files WindowispARCHIVED FILE
run-pet.ps1
503 KB · Oct 3, 2026 · 06:30 UTC
param(
[int]$PetWidth = 84,
[int]$PetHeight = 92,
[string]$AtlasPath = '',
[string]$PetId = '',
[string]$PetName = '',
[string]$EnemyAtlasPath = ''
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
$createdNew = $false
$singleInstance = [Threading.Mutex]::new($true, 'Local\WindowispControlPrototype', [ref]$createdNew)
if (-not $createdNew) {
[Windows.MessageBox]::Show('The controllable desktop pet is already running.', 'Windowisp') | Out-Null
$singleInstance.Dispose()
return
}
if (-not ('PetNative' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public static class PetNative {
public const int GWL_EXSTYLE = -20;
public const int GWL_STYLE = -16;
public const long WS_EX_TRANSPARENT = 0x20L;
public const long WS_EX_TOOLWINDOW = 0x80L;
public const long WS_CAPTION = 0x00C00000L;
public const int WM_HOTKEY = 0x0312;
public const uint MOD_ALT = 0x0001;
public const uint MOD_CONTROL = 0x0002;
public const uint MOD_NOREPEAT = 0x4000;
public const int DWMWA_CLOAKED = 14;
[StructLayout(LayoutKind.Sequential)] public struct RECT {
public int Left, Top, Right, Bottom;
}
[StructLayout(LayoutKind.Sequential)] public struct POINT {
public int X, Y;
}
public struct PLATFORMWINDOW {
public IntPtr Hwnd;
public int Left, Top, Right, Bottom;
}
public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hwnd);
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hwnd);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hwnd, out RECT rect);
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint pid);
[DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hwnd);
[DllImport("user32.dll")] public static extern bool RegisterHotKey(IntPtr hwnd, int id, uint modifiers, uint key);
[DllImport("user32.dll")] public static extern bool UnregisterHotKey(IntPtr hwnd, int id);
[DllImport("user32.dll")] public static extern short GetAsyncKeyState(int key);
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT point);
[DllImport("user32.dll", EntryPoint="GetWindowLongPtr")] public static extern IntPtr GetWindowLongPtr(IntPtr hwnd, int index);
[DllImport("user32.dll", EntryPoint="SetWindowLongPtr")] public static extern IntPtr SetWindowLongPtr(IntPtr hwnd, int index, IntPtr value);
[DllImport("dwmapi.dll")] public static extern int DwmGetWindowAttribute(IntPtr hwnd, int attribute, out int value, int size);
private static bool TryReadPlatformWindow(IntPtr hwnd, uint ownPid, out PLATFORMWINDOW platform) {
platform = new PLATFORMWINDOW();
uint pid;
GetWindowThreadProcessId(hwnd, out pid);
RECT r;
int cloaked = 0;
DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, out cloaked, sizeof(int));
long style = GetWindowLongPtr(hwnd, GWL_STYLE).ToInt64();
long exStyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE).ToInt64();
if (pid == ownPid || !IsWindowVisible(hwnd) || IsIconic(hwnd) || cloaked != 0 ||
GetWindowTextLength(hwnd) <= 0 || (style & WS_CAPTION) != WS_CAPTION ||
(exStyle & WS_EX_TOOLWINDOW) != 0 || !GetWindowRect(hwnd, out r) ||
r.Right - r.Left <= 80 || r.Bottom - r.Top <= 50) return false;
platform.Hwnd = hwnd;
platform.Left = r.Left; platform.Top = r.Top; platform.Right = r.Right; platform.Bottom = r.Bottom;
return true;
}
public static bool TryGetPlatformWindow(IntPtr hwnd, uint ownPid, out PLATFORMWINDOW platform) {
return TryReadPlatformWindow(hwnd, ownPid, out platform);
}
public static PLATFORMWINDOW[] GetPlatformWindows(uint ownPid) {
var result = new List<PLATFORMWINDOW>();
EnumWindows((hwnd, unused) => {
PLATFORMWINDOW platform;
if (TryReadPlatformWindow(hwnd, ownPid, out platform)) result.Add(platform);
return true;
}, IntPtr.Zero);
return result.ToArray();
}
}
'@
}
$basePetWidth = $PetWidth
$basePetHeight = $PetHeight
$petScalePercent = 85
$petScalePath = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Windowisp\pet-size.json'
if (Test-Path -LiteralPath $petScalePath -PathType Leaf) {
try {
$savedPetScale = Get-Content -LiteralPath $petScalePath -Raw | ConvertFrom-Json
if ([int]$savedPetScale.percent -in @(70,85,100,115)) { $petScalePercent = [int]$savedPetScale.percent }
} catch {}
}
$PetWidth = [int][Math]::Round($basePetWidth * ($petScalePercent / 100.0))
$PetHeight = [int][Math]::Round($basePetHeight * ($petScalePercent / 100.0))
$window = New-Object Windows.Window
$window.Title = 'Windowisp Player'
$window.Width = $PetWidth
$window.Height = $PetHeight
$window.WindowStyle = [Windows.WindowStyle]::None
$window.AllowsTransparency = $true
$window.Background = [Windows.Media.Brushes]::Transparent
$window.Topmost = $true
$window.ShowInTaskbar = $false
$window.ResizeMode = [Windows.ResizeMode]::NoResize
$window.Left = [Windows.SystemParameters]::VirtualScreenLeft + 80
$window.Top = [Windows.SystemParameters]::VirtualScreenTop + 80
$root = New-Object Windows.Controls.Grid
$pet = New-Object Windows.Controls.Canvas
$pet.Width = $PetWidth
$pet.Height = $PetHeight
$root.Children.Add($pet) | Out-Null
$window.Content = $root
if ([string]::IsNullOrWhiteSpace($AtlasPath)) {
throw 'No pet atlas was supplied. Start the pet with game-mode.ps1 so it can load your Codex pet.'
}
$script:atlas = $null
if (Test-Path -LiteralPath $AtlasPath) {
$bitmap = New-Object Windows.Media.Imaging.BitmapImage
$bitmap.BeginInit()
$bitmap.CacheOption = [Windows.Media.Imaging.BitmapCacheOption]::OnLoad
$bitmap.UriSource = [Uri](Resolve-Path -LiteralPath $AtlasPath).Path
$bitmap.EndInit()
$bitmap.Freeze()
if ($bitmap.PixelWidth -ne 1536 -or $bitmap.PixelHeight -ne 2288) {
throw "Pet atlas must be a v2 1536x2288 image; got $($bitmap.PixelWidth)x$($bitmap.PixelHeight)."
}
$script:atlas = $bitmap
}
if ([string]::IsNullOrWhiteSpace($EnemyAtlasPath)) {
$EnemyAtlasPath = Join-Path $PSScriptRoot 'assets\enemy\ember-imp-strip.png'
}
$enemyAtlases = @{}
$enemyFrameCache = @{}
$spriteFrameCache = @{}
$p2Atlas = $script:atlas
$p2SpriteFrameCache = @{}
foreach($enemyAtlasSpec in @(
@('EmberImp',$EnemyAtlasPath),
@('CinderMoth',(Join-Path $PSScriptRoot 'assets\enemy\cinder-moth-strip.png')),
@('MossHopper',(Join-Path $PSScriptRoot 'assets\enemy\moss-hopper-strip.png')),
@('ShellbackRoller',(Join-Path $PSScriptRoot 'assets\enemy\shellback-roller-strip.png')),
@('StormJelly',(Join-Path $PSScriptRoot 'assets\enemy\storm-jelly-strip.png')),
@('GloamGolem',(Join-Path $PSScriptRoot 'assets\enemy\gloam-golem-strip.png'))
)){
if(-not(Test-Path -LiteralPath $enemyAtlasSpec[1])){continue}
$enemyBitmap=New-Object Windows.Media.Imaging.BitmapImage
$enemyBitmap.BeginInit();$enemyBitmap.CacheOption=[Windows.Media.Imaging.BitmapCacheOption]::OnLoad
$enemyBitmap.UriSource=[Uri](Resolve-Path -LiteralPath $enemyAtlasSpec[1]).Path
$enemyBitmap.EndInit();$enemyBitmap.Freeze()
if(($enemyBitmap.PixelWidth%4)-ne0){throw "Enemy animation strip '$($enemyAtlasSpec[0])' must contain four equal-width frames."}
$enemyAtlases[$enemyAtlasSpec[0]]=$enemyBitmap
}
$enemyTypes=@{
EmberImp=@{Kind='FlyerShooter';Width=112.0;Height=104.0;Health=3;Speed=4.2;Score=1}
CinderMoth=@{Kind='FlyerShooter';Width=108.0;Height=100.0;Health=2;Speed=5.0;Score=1}
MossHopper=@{Kind='Hopper';Width=116.0;Height=92.0;Health=4;Speed=3.2;Score=2}
ShellbackRoller=@{Kind='Roller';Width=120.0;Height=88.0;Health=6;Speed=7.2;Score=2}
StormJelly=@{Kind='Swooper';Width=116.0;Height=106.0;Health=4;Speed=6.4;Score=2}
GloamGolem=@{Kind='Boss';Width=184.0;Height=168.0;Health=24;Speed=2.2;Score=10}
}
$enemyMovementScale=0.8
function Get-PetSpriteFrame([int]$row, [int]$frame) {
$key = "$row|$frame"
if (-not $script:spriteFrameCache.ContainsKey($key)) {
$crop = [Windows.Media.Imaging.CroppedBitmap]::new($script:atlas, [Windows.Int32Rect]::new(($frame * 192), ($row * 208), 192, 208))
$crop.Freeze()
$script:spriteFrameCache[$key] = $crop
}
return $script:spriteFrameCache[$key]
}
function Get-P2SpriteFrame([int]$row,[int]$frame) {
$key="$row|$frame"
if(-not$script:p2SpriteFrameCache.ContainsKey($key)){
$crop=[Windows.Media.Imaging.CroppedBitmap]::new($script:p2Atlas,[Windows.Int32Rect]::new(($frame*192),($row*208),192,208))
$crop.Freeze();$script:p2SpriteFrameCache[$key]=$crop
}
return $script:p2SpriteFrameCache[$key]
}
function Add-Ellipse([double]$x, [double]$y, [double]$w, [double]$h, $fill, $stroke = $null, [double]$thickness = 0) {
$shape = New-Object Windows.Shapes.Ellipse
$shape.Width = $w; $shape.Height = $h; $shape.Fill = $fill
if ($stroke) { $shape.Stroke = $stroke; $shape.StrokeThickness = $thickness }
[Windows.Controls.Canvas]::SetLeft($shape, $x)
[Windows.Controls.Canvas]::SetTop($shape, $y)
$pet.Children.Add($shape) | Out-Null
}
function Draw-Pet([bool]$active, [string]$state, [int]$facing, [int]$frame) {
$renderKey = "$active|$state|$facing|$frame"
if ($script:lastRenderKey -eq $renderKey) { return }
$script:lastRenderKey = $renderKey
$pet.Children.Clear()
if ($null -ne $atlas) {
$row = if ($state -eq 'attack') { 3 } elseif ($state -eq 'jump') { 4 } elseif ($state -eq 'run' -and $facing -lt 0) { 2 } elseif ($state -eq 'run') { 1 } else { 0 }
$sprite = New-Object Windows.Controls.Image
$sprite.Width = $PetWidth; $sprite.Height = $PetHeight
$sprite.Stretch = [Windows.Media.Stretch]::Uniform
$sprite.Source = Get-PetSpriteFrame $row $frame
$pet.Children.Add($sprite) | Out-Null
} else {
$outline = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(48, 37, 70))
$body = if ($active) { New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(130, 230, 255)) } else { New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(164, 148, 222)) }
$belly = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(241, 238, 255))
Add-Ellipse 13 18 58 66 $body $outline 3
Add-Ellipse 23 46 38 33 $belly
Add-Ellipse 25 35 9 12 ([Windows.Media.Brushes]::White) $outline 2
Add-Ellipse 50 35 9 12 ([Windows.Media.Brushes]::White) $outline 2
$pupilShift = if ($facing -lt 0) { -2 } elseif ($facing -gt 0) { 2 } else { 0 }
Add-Ellipse (28 + $pupilShift) 39 4 6 $outline
Add-Ellipse (53 + $pupilShift) 39 4 6 $outline
Add-Ellipse 18 76 24 12 $body $outline 3
Add-Ellipse 43 76 24 12 $body $outline 3
}
if ($state -eq 'attack') {
$charge = New-Object Windows.Shapes.Ellipse
$charge.Width = 16 + ($frame * 2); $charge.Height = $charge.Width
$charge.Fill = [Windows.Media.RadialGradientBrush]::new(
[Windows.Media.Colors]::White,
[Windows.Media.Color]::FromRgb(255, 72, 8)
)
[Windows.Controls.Canvas]::SetLeft($charge, $(if ($facing -lt 0) { 2 } else { $PetWidth - $charge.Width - 2 }))
[Windows.Controls.Canvas]::SetTop($charge, 32)
$pet.Children.Add($charge) | Out-Null
}
if ($script:twoPlayerActive) {
$badge = New-Object Windows.Controls.Border
$badge.Width = 25; $badge.Height = 18; $badge.CornerRadius = [Windows.CornerRadius]::new(8)
$badge.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(50, 151, 238))
$label = New-Object Windows.Controls.TextBlock
$label.Text = 'P1'; $label.Foreground = [Windows.Media.Brushes]::White; $label.FontSize = 10
$label.FontWeight = [Windows.FontWeights]::Bold; $label.HorizontalAlignment = 'Center'; $label.VerticalAlignment = 'Center'
$badge.Child = $label; [Windows.Controls.Canvas]::SetLeft($badge, $PetWidth - 28); [Windows.Controls.Canvas]::SetTop($badge, 2)
$pet.Children.Add($badge) | Out-Null
}
}
function Draw-Player2([string]$state, [int]$facing, [int]$frame) {
if ($null -eq $script:player2Canvas) { return }
$key = "$state|$facing|$frame"
if ($script:p2LastRenderKey -eq $key) { return }
$script:p2LastRenderKey = $key; $script:player2Canvas.Children.Clear()
$row = if ($state -eq 'attack') { 3 } elseif ($state -eq 'jump') { 4 } elseif ($state -eq 'run' -and $facing -lt 0) { 2 } elseif ($state -eq 'run') { 1 } else { 0 }
$sprite = New-Object Windows.Controls.Image
$sprite.Width = $PetWidth; $sprite.Height = $PetHeight; $sprite.Stretch = [Windows.Media.Stretch]::Uniform
$sprite.Source = Get-P2SpriteFrame $row $frame
if($null-eq$script:p2GlowEffect){
$script:p2GlowEffect=New-Object Windows.Media.Effects.DropShadowEffect
$script:p2GlowEffect.Color=[Windows.Media.Color]::FromRgb(236,67,190)
$script:p2GlowEffect.BlurRadius=18;$script:p2GlowEffect.ShadowDepth=0;$script:p2GlowEffect.Opacity=.95
if($script:p2GlowEffect.CanFreeze){$script:p2GlowEffect.Freeze()}
}
$sprite.Effect=$script:p2GlowEffect
$script:player2Canvas.Children.Add($sprite) | Out-Null
$badge = New-Object Windows.Controls.Border
$badge.Width = 25; $badge.Height = 18; $badge.CornerRadius = [Windows.CornerRadius]::new(8)
$badge.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(216, 48, 165))
$label = New-Object Windows.Controls.TextBlock
$label.Text = 'P2'; $label.Foreground = [Windows.Media.Brushes]::White; $label.FontSize = 10
$label.FontWeight = [Windows.FontWeights]::Bold; $label.HorizontalAlignment = 'Center'; $label.VerticalAlignment = 'Center'
$badge.Child = $label; [Windows.Controls.Canvas]::SetLeft($badge, $PetWidth - 28); [Windows.Controls.Canvas]::SetTop($badge, 2)
$script:player2Canvas.Children.Add($badge) | Out-Null
}
function Set-TwoPlayerMode([bool]$enabled) {
if ($script:twoPlayerActive -eq $enabled) { return }
$script:twoPlayerActive = $enabled
$script:p2GroundPlatformHwnd = [IntPtr]::Zero
$script:p2GroundPlatformRect = $null
$script:lastRenderKey = ''
if ($enabled) {
$script:player2Window = New-Object Windows.Window
$script:player2Window.Title = 'Windowisp Player 2'
$script:player2Window.Width = $PetWidth; $script:player2Window.Height = $PetHeight
$script:player2Window.WindowStyle = [Windows.WindowStyle]::None
$script:player2Window.AllowsTransparency = $true; $script:player2Window.Background = [Windows.Media.Brushes]::Transparent
$script:player2Window.ShowInTaskbar = $false; $script:player2Window.Topmost = $script:alwaysOnTop
$script:player2Window.ShowActivated = $false; $script:player2Window.ResizeMode = [Windows.ResizeMode]::NoResize
$script:player2Canvas = New-Object Windows.Controls.Canvas
$script:player2Window.Content = $script:player2Canvas
$script:p2X = $script:x + 110; $script:p2Y = $script:y
$script:p2VX = 0; $script:p2VY = 0; $script:p2Grounded = $false; $script:p2DropTicks = 0; $script:p2LastRenderKey = ''
$script:p2WallContact = 0; $script:p2WallGraceTicks = 0
$script:p2Hearts = 3; $script:p2InvulnerableTicks = 0
$script:player2Window.Left = $script:p2X; $script:player2Window.Top = $script:p2Y
$script:player2Window.Show(); Set-OverlayClickThrough $script:player2Window
Draw-Player2 'idle' 1 0
} else {
foreach ($ball in @($script:playroomBalls)) {
if ($ball.CarriedBy -eq 'Player2') { $ball.CarriedBy = ''; $ball.VX = 0; $ball.VY = 2 }
}
if ($null -ne $script:player2Window) { $script:player2Window.Close() }
$script:player2Window = $null; $script:player2Canvas = $null
}
Update-Menu
if ($null -ne $script:petSelectorWindow -and $script:petSelectorWindow.IsVisible) { Show-PetSelector }
}
function Set-Player2PresetBindings([string]$preset) {
if ($preset -eq 'Laptop') {
$script:controlBindings.P2Left=0x4A; $script:controlBindings.P2Right=0x4C
$script:controlBindings.P2Drop=0x4B; $script:controlBindings.P2Jump=0x49
$script:controlBindings.P2Attack=0x4F; $script:controlBindings.P2Interact=0x55
} else {
$script:controlBindings.P2Left=0x25; $script:controlBindings.P2Right=0x27
$script:controlBindings.P2Drop=0x28; $script:controlBindings.P2Jump=0x60
$script:controlBindings.P2Attack=0x6B; $script:controlBindings.P2Interact=0x61
}
Save-ControlBindings
Update-ControlBindingButtons
}
function Set-Player2Controller([string]$mode) {
if($mode-notin@('Human','Opponent','Friend')){$mode='Human'}
$script:p2ControllerMode=$mode
$script:p2AIIntent=[pscustomobject]@{Left=$false;Right=$false;Drop=$false;Jump=$false;Attack=$false;Interact=$false;Label='Waiting'}
$script:p2AIThinkTicks=0;$script:p2AIStuckTicks=0;$script:p2AILastX=$script:p2X;$script:p2AIStealCooldown=0
if($mode-ne'Human'-and-not$script:twoPlayerActive){Set-TwoPlayerMode $true}
Update-Menu
}
function Set-Player2AIDifficulty([string]$difficulty) {
if($difficulty-notin@('Friendly','Normal','Tricky')){$difficulty='Normal'}
$script:p2AIDifficulty=$difficulty
$script:p2AIThinkTicks=0
Update-Menu
}
function Try-Player2Steal($ball) {
if($null-eq$ball-or$ball.CarriedBy-ne'Player1'-or$script:p2AIStealCooldown-gt0){return $false}
$p1CentreX=$script:x+($PetWidth/2);$p1CentreY=$script:y+($PetHeight/2)
$p2CentreX=$script:p2X+($PetWidth/2);$p2CentreY=$script:p2Y+($PetHeight/2)
$distance=[Math]::Sqrt([Math]::Pow($p1CentreX-$p2CentreX,2)+[Math]::Pow($p1CentreY-$p2CentreY,2))
$settings=switch($script:p2AIDifficulty){
'Friendly'{[pscustomobject]@{Range=76.0;Chance=.28;Cooldown=110}}
'Tricky'{[pscustomobject]@{Range=105.0;Chance=.82;Cooldown=48}}
default{[pscustomobject]@{Range=90.0;Chance=.55;Cooldown=72}}
}
if($distance-gt$settings.Range){return $false}
$script:p2AIStealCooldown=$settings.Cooldown
if($script:random.NextDouble()-gt$settings.Chance){return $false}
$ball.CarriedBy='Player2';$ball.VX=0;$ball.VY=0;$ball.PetHitCooldown=18
$script:p2AIIntent.Label='Stole ball'
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:p2-steal difficulty=$($script:p2AIDifficulty) distance=$([Math]::Round($distance,1))")}
return $true
}
function Update-Player2AIIntent {
if($script:p2ControllerMode-eq'Human'){return}
if($script:p2AIThinkTicks-gt0){$script:p2AIThinkTicks--;return}
$script:p2AIThinkTicks=$(if($script:p2ControllerMode-eq'Friend'){7}else{switch($script:p2AIDifficulty){'Friendly'{8}'Tricky'{2}default{4}}})
$intent=[pscustomobject]@{Left=$false;Right=$false;Drop=$false;Jump=$false;Attack=$false;Interact=$false;Label='Waiting'}
$centreX=$script:p2X+($PetWidth/2);$centreY=$script:p2Y+($PetHeight/2)
$p1CarriedBall=@($script:playroomBalls|Where-Object{$_.Kind-eq'Ball'-and$_.CarriedBy-eq'Player1'}|Select-Object -First 1)
$p2CarriedBall=@($script:playroomBalls|Where-Object{$_.Kind-eq'Ball'-and$_.CarriedBy-eq'Player2'}|Select-Object -First 1)
$balls=@($script:playroomBalls|Where-Object{$_.Kind-eq'Ball'-and-not$_.CarriedBy})
$ball=$null
if($balls.Count-gt0){$ball=@($balls|Sort-Object{[Math]::Abs(($_.X+$_.Radius)-$centreX)+[Math]::Abs(($_.Y+$_.Radius)-$centreY)}|Select-Object -First 1)[0]}
$targetX=$script:x+($PetWidth/2);$targetY=$script:y+($PetHeight/2);$deadZone=24.0
if($script:p2ControllerMode-eq'Opponent'-and$p2CarriedBall.Count-gt0){
$targetX=[Windows.SystemParameters]::VirtualScreenLeft+55;$targetY=$centreY;$deadZone=18;$intent.Label='Drive to goal'
if($centreX-lt[Windows.SystemParameters]::VirtualScreenLeft+150){$intent.Interact=$true;$intent.Label='Shoot'}
}elseif($script:p2ControllerMode-eq'Opponent'-and$p1CarriedBall.Count-gt0){
$targetX=$script:x+($PetWidth/2);$targetY=$script:y+($PetHeight/2);$deadZone=10;$intent.Label='Challenge P1'
Try-Player2Steal $p1CarriedBall[0]|Out-Null
}elseif($script:p2ControllerMode-eq'Opponent'-and$null-ne$ball){
# P2 attacks left. Approach from the ball's right, then keep driving through it.
$ballX=$ball.X+$ball.Radius;$ballY=$ball.Y+$ball.Radius
$behindX=$ballX+$ball.Radius+28
$targetX=$(if($centreX-ge$ballX-8){$ballX-70}else{$behindX})
$targetY=$ballY;$deadZone=14;$intent.Label=$(if($centreX-ge$ballX-8){'Attack goal'}else{'Get behind ball'})
}elseif($script:p2ControllerMode-eq'Friend'){
$followOffset=$(if($script:facing-lt0){70}else{-70})
$targetX=$script:x+($PetWidth/2)+$followOffset;$targetY=$script:y+($PetHeight/2);$deadZone=38;$intent.Label='Follow P1'
if($null-ne$ball-and[Math]::Abs(($ball.X+$ball.Radius)-($script:x+$PetWidth/2))-lt230){
$targetX=$ball.X+$ball.Radius;$targetY=$ball.Y+$ball.Radius;$deadZone=20;$intent.Label='Help with ball'
}
}
$dx=$targetX-$centreX
if($dx-lt-$deadZone){$intent.Left=$true}elseif($dx-gt$deadZone){$intent.Right=$true}
if([Math]::Abs($script:p2X-$script:p2AILastX)-lt1.2-and($intent.Left-or$intent.Right)){$script:p2AIStuckTicks++}else{$script:p2AIStuckTicks=0}
$script:p2AILastX=$script:p2X
$targetAbove=$targetY-lt($centreY-45)
$moveDirection=$(if($intent.Left){-1}elseif($intent.Right){1}else{0})
$obstacleAhead=$false
if($moveDirection-ne0){
$probeX=$(if($moveDirection-lt0){$script:p2X-34}else{$script:p2X+$PetWidth+34})
foreach($block in @($script:playroomBlocks|Where-Object{$_.Type-ne'Ladder'})){
if($probeX-ge$block.X-8-and$probeX-le$block.X+$block.Width+8-and$centreY-gt$block.Y-and$script:p2Y+8-lt$block.Y+$block.Height){$obstacleAhead=$true;break}
}
}
if($script:p2Grounded-and($targetAbove-or$obstacleAhead-or$script:p2AIStuckTicks-ge5)){
$intent.Jump=$true;$script:p2AIStuckTicks=0
if($obstacleAhead){$intent.Label='Jump obstacle'}
}elseif(-not$script:p2Grounded-and$script:p2AirJumps-gt0-and$script:p2VY-gt-6-and($targetY-lt$centreY-75-or$obstacleAhead)){
$doubleJumpChance=$(switch($script:p2AIDifficulty){'Friendly'{.45}'Tricky'{.95}default{.75}})
if($script:p2ControllerMode-eq'Friend'){$doubleJumpChance=.65}
if($script:random.NextDouble()-le$doubleJumpChance){$intent.Jump=$true;$intent.Label='Double jump'}
}
$script:p2AIIntent=$intent
}
function Get-LadderContact([double]$playerX,[double]$playerY) {
$centreX=$playerX+($PetWidth/2);$centreY=$playerY+($PetHeight/2)
foreach($ladder in @($script:playroomBlocks|Where-Object{$_.Type-eq'Ladder'})){
$ladderCentreX=$ladder.X+($ladder.Width/2);$ladderCentreY=$ladder.Y+($ladder.Height/2)
$r=-$ladder.Angle*[Math]::PI/180
$dx=$centreX-$ladderCentreX;$dy=$centreY-$ladderCentreY
$localX=($dx*[Math]::Cos($r))-($dy*[Math]::Sin($r))
$localY=($dx*[Math]::Sin($r))+($dy*[Math]::Cos($r))
# Keep hold of the ladder above its final rung long enough to clear the
# platform lip. The larger top/bottom envelope also makes short ladders
# forgiving without widening their sideways grab area.
if([Math]::Abs($localX)-le($ladder.Width/2+10)-and$localY-ge(-$ladder.Height/2-($PetHeight*.62))-and$localY-le($ladder.Height/2+($PetHeight*.28))){
$angle=$ladder.Angle*[Math]::PI/180
return [pscustomobject]@{Block=$ladder;UpX=[Math]::Sin($angle);UpY=-[Math]::Cos($angle)}
}
}
return $null
}
function Test-LadderBlockPassThrough($ladderContact,$block) {
# A ladder may open a corridor through an explicit one-way platform, but it
# must never turn a nearby solid block intangible while the player climbs.
if($null-eq$ladderContact-or$null-eq$block-or$block.Type-notin@('Wooden','Cloud')){return $false}
$ladder=$ladderContact.Block
# Keep the opening narrow so the rest of the one-way platform remains usable.
$corridorLeft=$ladder.X-8;$corridorRight=$ladder.X+$ladder.Width+8
$blockLeft=$block.X;$blockRight=$block.X+$block.Width
$ladderTop=$ladder.Y-($PetHeight*.62);$ladderBottom=$ladder.Y+$ladder.Height+($PetHeight*.28)
return $blockRight-gt$corridorLeft-and$blockLeft-lt$corridorRight-and($block.Y+$block.Height)-gt$ladderTop-and$block.Y-lt$ladderBottom
}
function Test-PlatformTopExposed([int]$platformIndex,[double]$bodyLeft,[double]$bodyRight,[double]$platformTop) {
$sampleX=($bodyLeft+$bodyRight)/2
for($frontIndex=0;$frontIndex-lt$platformIndex;$frontIndex++){
$front=$script:platforms[$frontIndex]
if($sampleX-gt$front.Left-and$sampleX-lt$front.Right-and$platformTop-gt$front.Top-and$platformTop-lt$front.Bottom){return $false}
}
return $true
}
function Update-RidingApplicationWindow([string]$player) {
$isP2=$player-eq'P2'
$grounded=$(if($isP2){$script:p2Grounded}else{$script:grounded})
$hwnd=$(if($isP2){$script:p2GroundPlatformHwnd}else{$script:groundPlatformHwnd})
$previous=$(if($isP2){$script:p2GroundPlatformRect}else{$script:groundPlatformRect})
if(-not$grounded-or$hwnd-eq[IntPtr]::Zero-or$null-eq$previous){return}
try{
$current=[PetNative+PLATFORMWINDOW]::new()
if(-not[PetNative]::TryGetPlatformWindow($hwnd,[uint32]$PID,[ref]$current)){throw 'Application window is no longer rideable.'}
$dx=[double]$current.Left-[double]$previous.Left;$dy=[double]$current.Top-[double]$previous.Top
$maxWindowStep=[Math]::Max([Windows.SystemParameters]::VirtualScreenWidth,[Windows.SystemParameters]::VirtualScreenHeight)
if([double]::IsNaN($dx)-or[double]::IsNaN($dy)-or[Math]::Abs($dx)-gt$maxWindowStep-or[Math]::Abs($dy)-gt$maxWindowStep){throw 'Application window moved outside the desktop.'}
if($isP2){$script:p2X+=$dx;$script:p2Y+=$dy;$script:p2GroundPlatformRect=$current}
else{$script:x+=$dx;$script:y+=$dy;$script:groundPlatformRect=$current}
# The platform list can be replaced by the periodic desktop scan during a
# window drag. Updating it is only a cache optimisation, so never let a
# stale index interrupt movement input.
for($platformIndex=0;$platformIndex-lt@($script:platforms).Count;$platformIndex++){
if($script:platforms[$platformIndex].Hwnd-eq$hwnd){$script:platforms[$platformIndex]=$current;break}
}
}catch{
if($isP2){$script:p2GroundPlatformHwnd=[IntPtr]::Zero;$script:p2GroundPlatformRect=$null;$script:p2Grounded=$false}
else{$script:groundPlatformHwnd=[IntPtr]::Zero;$script:groundPlatformRect=$null;$script:grounded=$false}
try{$script:platforms=[PetNative]::GetPlatformWindows([uint32]$PID)}catch{$script:platforms=@()}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){Write-Output "diagnostic:window-ride-detached player=$player reason=$($_.Exception.Message)"}
return
}
}
function Update-Player2 {
if (-not $script:twoPlayerActive -or $null -eq $script:player2Window -or -not $script:active -or $script:p2Hearts -le 0) { return }
Update-RidingApplicationWindow 'P2'
if($script:p2AIStealCooldown-gt0){$script:p2AIStealCooldown--}
Update-Player2AIIntent
if($script:p2ControllerMode-eq'Human'){
$leftDown=Test-KeyDown $script:controlBindings.P2Left;$rightDown=Test-KeyDown $script:controlBindings.P2Right
$dropDown=Test-KeyDown $script:controlBindings.P2Drop;$jumpDown=Test-Player2JumpDown
$attackDown=Test-KeyDown $script:controlBindings.P2Attack;$interactDown=Test-KeyDown $script:controlBindings.P2Interact
}else{
$leftDown=$script:p2AIIntent.Left;$rightDown=$script:p2AIIntent.Right;$dropDown=$script:p2AIIntent.Drop
$jumpDown=$script:p2AIIntent.Jump;$attackDown=$script:p2AIIntent.Attack;$interactDown=$script:p2AIIntent.Interact
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_P2_JUMP-eq'1'-and-not$script:p2JumpTestTriggered){
$script:p2JumpTestTriggered=$true;$script:p2Grounded=$true;$script:p2CoyoteTicks=6;$script:p2Previous.Jump=$false;$jumpDown=$true
}
$p2Ladder=Get-LadderContact $script:p2X $script:p2Y
$p2Climbing=$null-ne$p2Ladder
$p2ClimbUpDown=(Test-KeyDown $vk.Up)-or$jumpDown
$horizontal = if ($leftDown -and -not $rightDown) { -1 } elseif ($rightDown -and -not $leftDown) { 1 } else { 0 }
if ($horizontal -ne 0) {
$p2MaxSpeed=9.5*$script:baseMovementScale
$p2Acceleration=$(if($script:p2Grounded){1.35}else{.72})*$script:baseMovementScale
$script:p2VX=[Math]::Max(-$p2MaxSpeed,[Math]::Min($p2MaxSpeed,$script:p2VX+($horizontal*$p2Acceleration)))
$script:p2Facing = $horizontal
} elseif ($script:p2Grounded) { $script:p2VX *= 0.78 } else { $script:p2VX *= 0.985 }
$p2LadderJump=$p2Climbing-and$jumpDown-and-not$script:p2Previous.Jump-and$horizontal-ne0
if($p2LadderJump){
$script:p2VY=-15.5*$script:baseMovementScale;$script:p2VX=$horizontal*11.5*$script:baseMovementScale
$script:p2Grounded=$false;$p2Climbing=$false;$p2Ladder=$null
}elseif ($jumpDown -and -not $script:p2Previous.Jump -and -not$p2Climbing) {
if ($script:p2Grounded -or $script:p2CoyoteTicks -gt 0) { $script:p2VY = -18.5*$script:baseMovementScale; $script:p2Grounded = $false; $script:p2AirJumps = 1 }
elseif ($script:p2WallGraceTicks -gt 0 -and $script:p2WallContact -ne 0) {
$script:p2VY = -17.5*$script:baseMovementScale; $script:p2VX = -$script:p2WallContact * 13.5*$script:baseMovementScale
$script:p2Facing = -$script:p2WallContact; $script:p2WallGraceTicks = 0
}
elseif ($script:p2AirJumps -gt 0) { $script:p2VY = -17*$script:baseMovementScale; $script:p2AirJumps-- }
}
if($script:p2JumpTestTriggered-and-not$script:p2JumpTestReported){
$script:p2JumpTestReported=$true
[Console]::Out.WriteLine("diagnostic:p2-jump controller=$($script:p2ControllerMode) binding=$($script:controlBindings.P2Jump) vy=$([Math]::Round($script:p2VY,2)) grounded=$($script:p2Grounded)")
}
if($p2Climbing){
$climbDirection=$(if($p2ClimbUpDown){1}elseif($dropDown){-1}else{0})
$axisVelocity=($script:p2VX*$p2Ladder.UpX)+($script:p2VY*$p2Ladder.UpY)
$targetAxisVelocity=$climbDirection*5.5*$script:baseMovementScale
$script:p2VX+=($targetAxisVelocity-$axisVelocity)*$p2Ladder.UpX
$script:p2VY+=($targetAxisVelocity-$axisVelocity)*$p2Ladder.UpY
$script:p2Grounded=$false
}
if ($dropDown -and -not $script:p2Previous.Drop) {
$script:p2DropTicks=18;$script:p2VY=[Math]::Max(4,$script:p2VY);$script:p2Grounded=$false
$script:p2GroundPlatformHwnd=[IntPtr]::Zero;$script:p2GroundPlatformRect=$null
}
if ($attackDown -and -not $script:p2Previous.Attack -and $script:p2AttackCooldown -le 0) {
if(-not(Start-BatCharge 'Player2')){
New-Fireball -owner 'Player2' -originX ($script:p2X + $(if ($script:p2Facing -lt 0) { -30 } else { $PetWidth - 8 })) -originY ($script:p2Y + 28) -velocityX (15 * $script:p2Facing)
$script:p2AttackTicks = 18; $script:p2AttackCooldown = 24
}
}
if($attackDown){Update-BatCharge 'Player2'}
if(-not$attackDown-and$script:p2Previous.Attack-and(Release-BatSwing 'Player2')){$script:p2AttackTicks=18;$script:p2AttackCooldown=22}
if ($interactDown -and -not $script:p2Previous.Interact) { Toggle-BallCarry 'Player2' }
$script:p2Previous.Jump=$jumpDown; $script:p2Previous.Drop=$dropDown; $script:p2Previous.Attack=$attackDown; $script:p2Previous.Interact=$interactDown
# AI actions are pulses, not held keys. Releasing after one physics tick
# allows a later airborne decision to trigger the real second-jump path.
if($script:p2ControllerMode-ne'Human'-and$jumpDown){$script:p2AIIntent.Jump=$false}
if ($script:p2AttackCooldown -gt 0) { $script:p2AttackCooldown-- }; if ($script:p2AttackTicks -gt 0) { $script:p2AttackTicks-- }
$oldX = $script:p2X; $oldY = $script:p2Y; $oldBottom = $script:p2Y + $PetHeight
$script:p2VY = [Math]::Min(24, $script:p2VY + $(if($p2Climbing){0}else{1.05})); $script:p2X += $script:p2VX; $script:p2Y += $script:p2VY
if($script:p2DropTicks-gt0){$script:p2DropTicks--}
$left=[Windows.SystemParameters]::VirtualScreenLeft; $top=[Windows.SystemParameters]::VirtualScreenTop
$right=$left+[Windows.SystemParameters]::VirtualScreenWidth-$PetWidth; $floor=$top+[Windows.SystemParameters]::VirtualScreenHeight
if ($script:p2WallGraceTicks -gt 0) { $script:p2WallGraceTicks-- } else { $script:p2WallContact = 0 }
if ($script:p2X -le $left -and $script:p2VX -lt 0) {
$script:p2X=$left;$script:p2VX=0;$script:p2WallContact=-1;$script:p2WallGraceTicks=6
if($script:p2VY-gt4.5){$script:p2VY=4.5}
} elseif ($script:p2X -ge $right -and $script:p2VX -gt 0) {
$script:p2X=$right;$script:p2VX=0;$script:p2WallContact=1;$script:p2WallGraceTicks=6
if($script:p2VY-gt4.5){$script:p2VY=4.5}
}
$script:p2X=[Math]::Max($left,[Math]::Min($right,$script:p2X)); $script:p2Y=[Math]::Max($top,$script:p2Y)
foreach ($b in @($script:playroomBlocks)) {
if($b.Type-in@('Ladder','Wooden','Cloud')-or$b.TemporaryState-eq'Gone'){continue}
if(Test-LadderBlockPassThrough $p2Ladder $b){continue}
if([Math]::Abs($b.Angle)-gt.01){continue}
$oldBodyTop=$oldY+8;$newBodyTop=$script:p2Y+8
$newLeft=$script:p2X+12;$newRight=$script:p2X+$PetWidth-12
$horizontalOverlap=$newRight-gt$b.X -and $newLeft-lt$b.X+$b.Width
if($script:p2VY-lt0-and$horizontalOverlap-and$oldBodyTop-ge$b.Y+$b.Height-6-and$newBodyTop-le$b.Y+$b.Height){
$script:p2Y=$b.Y+$b.Height-8;$script:p2VY=0;continue
}
$bodyTop=$script:p2Y+8;$bodyBottom=$script:p2Y+$PetHeight-5
if($bodyBottom-le$b.Y -or $bodyTop-ge$b.Y+$b.Height){continue}
$oldLeft=$oldX+12;$oldRight=$oldX+$PetWidth-12
if($script:p2VX-gt0 -and $oldRight-le$b.X+5 -and $newRight-ge$b.X){
$script:p2X=$b.X-($PetWidth-12);$script:p2VX=0;$script:p2WallContact=1;$script:p2WallGraceTicks=6
if($script:p2VY-gt4.5){$script:p2VY=4.5}
}elseif($script:p2VX-lt0 -and $oldLeft-ge$b.X+$b.Width-5 -and $newLeft-le$b.X+$b.Width){
$script:p2X=$b.X+$b.Width-12;$script:p2VX=0;$script:p2WallContact=-1;$script:p2WallGraceTicks=6
if($script:p2VY-gt4.5){$script:p2VY=4.5}
}
}
$landing=$floor;$p2LandingBlock=$null;$p2LandingPlatform=$null
if ($script:p2VY -ge 0) {
if($script:p2DropTicks-le0){for($platformIndex=0;$platformIndex-lt$script:platforms.Count;$platformIndex++){$r=$script:platforms[$platformIndex];$bodyLeft=$script:p2X+12;$bodyRight=$script:p2X+$PetWidth-12;if($bodyRight-gt$r.Left-and$bodyLeft-lt$r.Right-and$oldBottom-le$r.Top+5-and$script:p2Y+$PetHeight-ge$r.Top-and$r.Top-lt$landing-and(Test-PlatformTopExposed $platformIndex $bodyLeft $bodyRight $r.Top)){$landing=$r.Top;$p2LandingPlatform=$r}}}
foreach($b in @($script:playroomBlocks)){if($b.TemporaryState-ne'Gone'-and$b.Type-notin@('Ladder','Wooden','Cloud')-and-not(Test-LadderBlockPassThrough $p2Ladder $b)-and[Math]::Abs($b.Angle)-le.01-and$script:p2X+$PetWidth-12-gt$b.X -and $script:p2X+12-lt$b.X+$b.Width -and $oldBottom-le$b.Y+6 -and $script:p2Y+$PetHeight-ge$b.Y -and $b.Y-lt$landing){$landing=$b.Y;$p2LandingBlock=$b;$p2LandingPlatform=$null}}
}
$script:p2Grounded=$false
if($script:p2Y+$PetHeight-ge$landing){
$script:p2Y=$landing-$PetHeight;$script:p2VY=0;$script:p2Grounded=$true
if($null-ne$p2LandingPlatform){$script:p2GroundPlatformHwnd=$p2LandingPlatform.Hwnd;$script:p2GroundPlatformRect=$p2LandingPlatform}else{$script:p2GroundPlatformHwnd=[IntPtr]::Zero;$script:p2GroundPlatformRect=$null}
if($null-ne$p2LandingBlock){Trigger-TemporaryBlock $p2LandingBlock 'Player2'}
if($null-ne$p2LandingBlock-and$p2LandingBlock.Type-eq'Conveyor'){$script:p2VX=[Math]::Max(-13,[Math]::Min(13,$script:p2VX+($p2LandingBlock.Direction*.72)))}
}else{$script:p2GroundPlatformHwnd=[IntPtr]::Zero;$script:p2GroundPlatformRect=$null}
foreach($b in @($script:playroomBlocks|Where-Object{$_.Type-notin@('Ladder','Wooden','Cloud')-and[Math]::Abs($_.Angle)-gt.01-and-not(Test-LadderBlockPassThrough $p2Ladder $_)})){
$resolved=Resolve-PlayerRotatedBlock $b $script:p2X $script:p2Y $script:p2VX $script:p2VY
if($null-ne$resolved){
$script:p2X=$resolved.X;$script:p2Y=$resolved.Y;$script:p2VX=$resolved.VX;$script:p2VY=$resolved.VY
if($resolved.Grounded){
$script:p2Grounded=$true;$script:p2GroundPlatformHwnd=[IntPtr]::Zero;$script:p2GroundPlatformRect=$null
if($b.Type-in@('Bouncy','Fire')){
$launch=$(if($b.Type-eq'Bouncy'){20.5}else{13.0});$angle=$b.Angle*[Math]::PI/180
$script:p2VX+=[Math]::Sin($angle)*$launch;$script:p2VY-=[Math]::Cos($angle)*$launch;$script:p2Grounded=$false
}elseif($b.Type-eq'Conveyor'){
$angle=$b.Angle*[Math]::PI/180
$script:p2VX=[Math]::Max(-13,[Math]::Min(13,$script:p2VX+([Math]::Cos($angle)*$b.Direction*.72)))
$script:p2VY+=[Math]::Sin($angle)*$b.Direction*.72
}
}
if($resolved.Wall-ne0){$script:p2WallContact=$resolved.Wall;$script:p2WallGraceTicks=6}
}
}
foreach($wood in @($script:playroomBlocks|Where-Object{$_.Type-in@('Wooden','Cloud')})){
$woodLanding=Resolve-PlayerWoodenPlatform $wood $oldX $oldY $script:p2X $script:p2Y $script:p2VX $script:p2VY $script:p2DropTicks
if($null-ne$woodLanding){$script:p2X=$woodLanding.X;$script:p2Y=$woodLanding.Y;$script:p2VX=$woodLanding.VX;$script:p2VY=$woodLanding.VY;if($woodLanding.Grounded){$script:p2Grounded=$true;$script:p2GroundPlatformHwnd=[IntPtr]::Zero;$script:p2GroundPlatformRect=$null;if($wood.Type-eq'Cloud'){Trigger-TemporaryBlock $wood 'Player2'}}}
}
if($script:p2Grounded){$script:p2CoyoteTicks=6;$script:p2AirJumps=1}elseif($script:p2CoyoteTicks-gt0){$script:p2CoyoteTicks--}
$script:player2Window.Left=$script:p2X; $script:player2Window.Top=$script:p2Y
$state=if($script:p2AttackTicks-gt0){'attack'}elseif(-not$script:p2Grounded){'jump'}elseif([Math]::Abs($script:p2VX)-gt0.5){'run'}else{'idle'}
if($state-ne$script:p2LastState){$script:p2AnimationTick=0;$script:p2LastState=$state}else{$script:p2AnimationTick++}
$frame=if($state-eq'run'){[Math]::Floor($script:p2AnimationTick/7)%8}elseif($state-eq'jump'){if($script:p2VY-lt-8){1}elseif($script:p2VY-lt2){2}elseif($script:p2VY-lt12){3}else{4}}elseif($state-eq'attack'){[Math]::Min(3,[Math]::Floor($script:p2AnimationTick/3))}else{[Math]::Floor($script:p2AnimationTick/10)%6}
Draw-Player2 $state $script:p2Facing ([int]$frame)
}
$active = $false
$x = [double]$window.Left
$y = [double]$window.Top
$vx = 0.0; $vy = 0.0
$facing = 0
$grounded = $false
$dropTicks = 0
$wallContact = 0
$wallGraceTicks = 0
$platforms = @()
$platformRefresh = 0
$groundPlatformHwnd = [IntPtr]::Zero
$groundPlatformRect = $null
$p2GroundPlatformHwnd = [IntPtr]::Zero
$p2GroundPlatformRect = $null
$animationTick = 0
$lastState = 'idle'
$lastRenderKey = ''
$hotkeyIds = @{ Toggle = 1; Left = 2; Right = 3; JumpW = 4; Drop = 5; JumpSpace = 6; Exit = 7; Attack = 8; Reset = 9; Close = 10; Menu = 11; Freeze = 12 }
$vk = @{ F7 = 0x76; F8 = 0x77; F9 = 0x78; F10 = 0x79; A = 0x41; D = 0x44; W = 0x57; S = 0x53; E = 0x45; U = 0x55; Space = 0x20; Escape = 0x1B; J = 0x4A; P = 0x50; Left = 0x25; Up = 0x26; Right = 0x27; Down = 0x28; Num0 = 0x60; Num1 = 0x61; NumAdd = 0x6B; I = 0x49; K = 0x4B; L = 0x4C; O = 0x4F }
$previousKeys = @{ Jump = $false; Drop = $false; Attack = $false; Interact = $false; Left = $false; Right = $false }
$coyoteTicks = 0
$attackTicks = 0
$attackCooldown = 0
$chargeTicks = 0
$charging = $false
$airJumpsRemaining = 1
$inputTick = 0
$lastLeftTapTick = -100
$lastRightTapTick = -100
$dashTicks = 0
$dashCooldown = 0
$projectiles = New-Object Collections.ArrayList
$enemies = New-Object Collections.ArrayList
$enemySpawnTicks = 210
$enemyNextSpawnAt = [DateTime]::UtcNow.AddSeconds(5)
$enemiesDefeated = 0
$lastBossWave = 0
$score = 0
$hearts = 3
$elapsedTicks = 0
$invulnerableTicks = 0
$hudWindow = $null
$hudText = $null
$hudRefreshTick = 0
$heartWindow = $null
$heartText = $null
$p1StatusWindow = $null; $p1StatusText = $null
$p2StatusWindow = $null; $p2StatusText = $null; $p2StatusPortrait = $null; $p1StatusPortrait = $null
$menuButtonWindow = $null
$menuWindow = $null
$petSelectorButton = $null
$petSelectorWindow = $null
$petSelectorPanel = $null
$petSizeButtons = @()
$petSelectorTarget = 'P1'
$activePetId = $PetId
$activePetName = $(if([string]::IsNullOrWhiteSpace($PetName)){$PetId}else{$PetName})
$p2PetId = $PetId
$p2PetName = $(if([string]::IsNullOrWhiteSpace($PetName)){$PetId}else{$PetName})
$controlsWindow = $null
$controlsOverlay = $null
$controlsBindingButtons = @{}
$capturingBinding = $null
$menuTitle = $null
$menuActionButton = $null
$menuRestartButton = $null
$gameModePanel = $null
$menuFreezeButton = $null
$menuSpeedButton = $null
$menuWanderButton = $null
$menuTopmostButton = $null
$menuFamiliarButton = $null
$settingsPanel = $null
$recoveryPanel = $null
$tutorialWindow = $null
$startupModeChosen = $false
$tutorialSeenPath = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Windowisp\tutorial-v1.seen'
$playroomPanel = $null
$playroomButton = $null
$sandboxToolButton = $null
$sandboxHotbarWindow = $null
$sandboxTrayWindow = $null
$sandboxTrayPanel = $null
$sandboxTrayTitle = $null
$sandboxPhysicsLabel = $null
$sandboxPhysicsSlider = $null
$sandboxCategory = ''
$sandboxToolboxHasCustomPosition = $false
$eraserMode = $false
$eraserButton = $null
$eraserEscapeWasDown = $false
$eraserExitGuardTicks = 0
$gridSnapEnabled = $false
$gridButton = $null
$gridOverlayWindow = $null
$gridSize = 40.0
$ropeToolMode = $false
$ropeToolButton = $null
$wiringMode = $false
$wiringButton = $null
$wireConnectFirst = $null
$wireChannelIndex = 0
$autoWander = $false
$soundEnabled = $true
$petSpeedIndex = 1
$petSpeedNames = @('Slow', 'Normal', 'Fast')
$petSpeedScales = @(0.75, 1.0, 1.25)
$baseMovementScale = 0.85
$alwaysOnTop = $true
$frozen = $false
$familiarBubbleWindow=$null;$familiarBubbleText=$null;$familiarBubbleTicks=0;$familiarBubblePersistent=$false
$familiarLastEvent='';$familiarLastEventAt=[DateTime]::MinValue;$familiarMode='Normal'
$familiarSettingsPath=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Windowisp\familiar-settings.json'
if(Test-Path -LiteralPath $familiarSettingsPath){try{$savedFamiliar=Get-Content -LiteralPath $familiarSettingsPath -Raw|ConvertFrom-Json;if($savedFamiliar.mode-in@('Quiet','Normal','Chatty')){$familiarMode=[string]$savedFamiliar.mode}}catch{}}
$twoPlayerActive = $false
$player2Window = $null
$player2Canvas = $null
$p2X = 0.0; $p2Y = 0.0; $p2VX = 0.0; $p2VY = 0.0
$p2Facing = 1; $p2Grounded = $false; $p2CoyoteTicks = 0; $p2AirJumps = 1; $p2DropTicks = 0
$p2WallContact = 0; $p2WallGraceTicks = 0
$p2AnimationTick = 0; $p2LastState = 'idle'; $p2LastRenderKey = ''
$p2AttackCooldown = 0; $p2AttackTicks = 0
$p2Hearts = 3; $p2InvulnerableTicks = 0
$p2Previous = @{ Jump=$false; Drop=$false; Attack=$false; Interact=$false }
$p2JumpTestTriggered = $false; $p2JumpTestReported = $false
$springAllP1Reported = $false; $springAllP2Reported = $false
$p2Preset = 'Numpad'
$p2MenuButton = $null
$p2PresetButton = $null
$p2ControllerMode = 'Human'
$p2ControllerButton = $null
$p2AIDifficulty = 'Normal'
$p2AIDifficultyButton = $null
$p2AIStealCooldown = 0
$p2AIIntent = [pscustomobject]@{Left=$false;Right=$false;Drop=$false;Jump=$false;Attack=$false;Interact=$false;Label='Waiting'}
$p2AIThinkTicks = 0; $p2AIStuckTicks = 0; $p2AILastX = 0.0
$p2AITestInitialized = $false
$enemyRosterTestInitialized = $false
$p2GlowEffect = $null
$playroomActive = $false
$playroomBlocks = New-Object Collections.ArrayList
$playroomBalls = New-Object Collections.ArrayList
$playroomGadgets = New-Object Collections.ArrayList
$playroomWires = New-Object Collections.ArrayList
$ropeConnectFirst = $null
$ropeConnectSource = $null
$ropeOverlayWindow = $null
$ropeOverlayCanvas = $null
$activeRotationHandleWindow = $null
$activeRotationData = $null
$teleportCooldown = 0
$p2TeleportCooldown = 0
$hazardTick = 0
$diagnosticSupportCases = @()
$gameType = 'Survival'
$footballActive = $false
$footballBall = $null
$footballLeftGoal = $null; $footballRightGoal = $null
$footballModeObjects = @()
$footballP1Score = 0; $footballP2Score = 0
$footballCountdownTicks = 0; $footballKickoffTextTicks = 0; $footballGoalCooldown = 0
$footballMessage = ''; $footballMatchOver = $false
$footballTestInitialized = $false
$wispfallActive=$false;$wispfallLavaWindow=$null;$wispfallLavaTop=0.0;$wispfallTicks=0;$wispfallScore=0
$wispfallSpawnTicks=0;$wispfallLastX=0.0;$wispfallLastLane=2;$wispfallLaneDirection=-1;$wispfallHighestY=0.0;$wispfallGameOver=$false;$wispfallTestInitialized=$false
$wispfallStoredBlocks=@();$wispfallStoredGadgets=@();$wispfallStoredWires=@();$wispfallPreviousScalePercent=$null
$crumbleTestBlock=$null;$crumbleTestStartTick=0
$dodgeballs = New-Object Collections.ArrayList
$dodgeballNextSpawnAt = [DateTime]::UtcNow.AddSeconds(5)
$dodgeballFromRight = $false
$ballPhysicsValue = 50.0
$ballPhysicsSlider = $null
$ballPhysicsLabel = $null
$surfaceEffect = 'Normal'
$surfaceEffectTicks = 0
$star = $null
$starSpawnTicks = 150
$starsCollected = 0
$shieldTicks = 0
$shieldWindow = $null
$shieldRing = $null
$shieldElement = $null
$shieldRotate = $null
$shieldAngle = 0.0
$shieldPickup = $null
$shieldPickupSpawnTicks = 45
$random = New-Object Random
$controlDefaults = @{ P1Left=0x41; P1Right=0x44; P1Jump=0x20; P1Drop=0x53; P1Attack=0x4A; P1Interact=0x45; P2Left=0x25; P2Right=0x27; P2Drop=0x28; P2Jump=0x60; P2Attack=0x6B; P2Interact=0x61 }
$controlBindings = $controlDefaults.Clone()
$controlsConfigPath = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Windowisp\controls.json'
if (Test-Path -LiteralPath $controlsConfigPath -PathType Leaf) {
try {
$savedControls = Get-Content -LiteralPath $controlsConfigPath -Raw | ConvertFrom-Json
foreach ($bindingName in @($controlDefaults.Keys)) {
if ($null -ne $savedControls.$bindingName) { $controlBindings[$bindingName] = [int]$savedControls.$bindingName }
}
} catch {}
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_RESET_CONTROLS -eq '1') {
$controlBindings = $controlDefaults.Clone()
try {
$controlsDirectory = Split-Path -Parent $controlsConfigPath
if (-not (Test-Path -LiteralPath $controlsDirectory -PathType Container)) { New-Item -ItemType Directory -Path $controlsDirectory -Force | Out-Null }
$controlBindings | ConvertTo-Json | Set-Content -LiteralPath $controlsConfigPath -Encoding UTF8
} catch {}
}
$platformHighlights = @{}
$frameClock = $(if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { [Diagnostics.Stopwatch]::StartNew() } else { $null })
$lastFrameMs = 0.0; $frameGapMax = 0.0; $frameGapSum = 0.0; $frameGapSamples = 0
$gadgetBenchmarkPhase='';$gadgetBenchmarkCpuStartMs=0.0;$gadgetBenchmarkWallStartMs=0.0;$gadgetBenchmarkStartHandles=0;$gadgetBenchmarkStartWindows=0
$diagnosticTickCount = 0
$diagnosticMainMarker = $false
$diagnosticLaserSegmentMarker = $false
$diagnosticLaserHitMarker = $false
$rotatorUpdateMarker = $false
$ropeVisualTick = 0
$ropeVisualDivisor = 1
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_DEFEATS) {
$parsedDefeats = 0
if ([int]::TryParse($env:WINDOWISP_TEST_DEFEATS, [ref]$parsedDefeats)) {
$enemiesDefeated = [Math]::Max(0, $parsedDefeats)
}
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output "diagnostic:initial-defeats=$enemiesDefeated" }
$source = $null
$hwnd = [IntPtr]::Zero
$gameOnEvent = [Threading.EventWaitHandle]::new($false, [Threading.EventResetMode]::AutoReset, 'Local\WindowispGameModeOn')
$gameOffEvent = [Threading.EventWaitHandle]::new($false, [Threading.EventResetMode]::AutoReset, 'Local\WindowispGameModeOff')
$gameToggleEvent = [Threading.EventWaitHandle]::new($false, [Threading.EventResetMode]::AutoReset, 'Local\WindowispGameModeToggle')
$gameExitEvent = [Threading.EventWaitHandle]::new($false, [Threading.EventResetMode]::AutoReset, 'Local\WindowispGameModeExit')
$playroomEvent = [Threading.EventWaitHandle]::new($false, [Threading.EventResetMode]::AutoReset, 'Local\WindowispPlayroom')
$petSwitchEvent = [Threading.EventWaitHandle]::new($false, [Threading.EventResetMode]::AutoReset, 'Local\WindowispSwitch')
$familiarEventSignal=[Threading.EventWaitHandle]::new($false,[Threading.EventResetMode]::AutoReset,'Local\WindowispFamiliarEvent')
function Set-ClickThrough([bool]$enabled) {
$style = [PetNative]::GetWindowLongPtr($hwnd, [PetNative]::GWL_EXSTYLE).ToInt64()
if ($enabled) { $style = $style -bor [PetNative]::WS_EX_TRANSPARENT }
else { $style = $style -band (-bnot [PetNative]::WS_EX_TRANSPARENT) }
[PetNative]::SetWindowLongPtr($hwnd, [PetNative]::GWL_EXSTYLE, [IntPtr]$style) | Out-Null
}
function Register-ControlKeys([bool]$enabled) {
foreach ($id in 2..8) { [PetNative]::UnregisterHotKey($hwnd, $id) | Out-Null }
if (-not $enabled) { return }
# Movement and attack are sampled with GetAsyncKeyState each frame. Registering
# repeating hotkeys as well floods WPF's dispatcher during combined key holds.
[PetNative]::RegisterHotKey($hwnd, $hotkeyIds.Exit, [PetNative]::MOD_NOREPEAT, $vk.Escape) | Out-Null
}
function Test-KeyDown([int]$key) {
return (([int][PetNative]::GetAsyncKeyState($key) -band 0x8000) -ne 0)
}
function Test-Player2JumpDown {
$binding=[int]$script:controlBindings.P2Jump
if(Test-KeyDown $binding){return $true}
# The physical Numpad 0 key reports as Insert while Num Lock is off.
# Treat both virtual-key values as the same default P2 jump button.
return $binding-eq0x60-and(Test-KeyDown 0x2D)
}
function New-Fireball(
[string]$owner = 'Player',
[double]$originX = [double]::NaN,
[double]$originY = [double]::NaN,
[double]$velocityX = [double]::NaN,
[double]$velocityY = 0,
[double]$power = 0
) {
$power = [Math]::Max(0, [Math]::Min(1, $power))
$scale = 1 + ($power * 0.8)
$direction = $(if (-not [double]::IsNaN($velocityX)) { [Math]::Sign($velocityX) } elseif ($script:facing -lt 0) { -1 } else { 1 })
$fireball = New-Object Windows.Window
$fireball.Title = "Windowisp $owner Fireball"
$fireball.Width = 54 * $scale; $fireball.Height = 38 * $scale
$fireball.WindowStyle = [Windows.WindowStyle]::None
$fireball.AllowsTransparency = $true
$fireball.Background = [Windows.Media.Brushes]::Transparent
$fireball.Topmost = $true; $fireball.ShowInTaskbar = $false
$fireball.ShowActivated = $false; $fireball.ResizeMode = [Windows.ResizeMode]::NoResize
$canvas = New-Object Windows.Controls.Canvas
$canvas.Width=54*$scale;$canvas.Height=38*$scale;$canvas.IsHitTestVisible=$false
$tail = New-Object Windows.Shapes.Ellipse
$tail.Width = 34 * $scale; $tail.Height = 17 * $scale
$tail.Fill = [Windows.Media.LinearGradientBrush]::new(
[Windows.Media.Color]::FromArgb(0, 255, 40, 0),
[Windows.Media.Color]::FromArgb(230, 255, 120, 0),
0
)
$core = New-Object Windows.Shapes.Ellipse
$core.Width = 31 * $scale; $core.Height = 31 * $scale
$outerColor = $(if ($owner -eq 'Enemy') { [Windows.Media.Color]::FromRgb(175, 35, 255) } else { [Windows.Media.Color]::FromRgb(255, 45, 0) })
$core.Fill = [Windows.Media.RadialGradientBrush]::new([Windows.Media.Colors]::White, $outerColor)
$core.Effect = New-Object Windows.Media.Effects.DropShadowEffect
$core.Effect.Color = [Windows.Media.Color]::FromRgb(255, 75, 0)
$core.Effect.BlurRadius = 16; $core.Effect.ShadowDepth = 0; $core.Effect.Opacity = 0.9
if ($direction -lt 0) {
[Windows.Controls.Canvas]::SetLeft($tail, 20 * $scale); [Windows.Controls.Canvas]::SetLeft($core, 1)
} else {
[Windows.Controls.Canvas]::SetLeft($tail, 0); [Windows.Controls.Canvas]::SetLeft($core, 22 * $scale)
}
[Windows.Controls.Canvas]::SetTop($tail, 10 * $scale); [Windows.Controls.Canvas]::SetTop($core, 3 * $scale)
$canvas.Children.Add($tail) | Out-Null; $canvas.Children.Add($core) | Out-Null
if ([double]::IsNaN($originX)) { $originX = $script:x + $(if ($direction -lt 0) { -38 } else { $PetWidth - 10 }) }
if ([double]::IsNaN($originY)) { $originY = $script:y + 28 }
if ([double]::IsNaN($velocityX)) { $velocityX = 15 * $direction }
$startX = $originX; $startY = $originY
Add-SharedGameplayElement $canvas $startX $startY
$script:projectiles.Add([pscustomobject]@{
Window = $null;Element=$canvas; X = [double]$startX; Y = [double]$startY
VX = [double]$velocityX; VY = [double]$velocityY; Life = 125; Owner = $owner
Width = [double](54*$scale); Height = [double](38*$scale)
Damage = $(if ($owner -eq 'Player') { 1 + [Math]::Floor($power * 2) } else { 1 })
}) | Out-Null
}
function Set-OverlayClickThrough($overlayWindow) {
$overlayHwnd = (New-Object Windows.Interop.WindowInteropHelper($overlayWindow)).Handle
$overlayStyle = [PetNative]::GetWindowLongPtr($overlayHwnd, [PetNative]::GWL_EXSTYLE).ToInt64() -bor [PetNative]::WS_EX_TRANSPARENT
[PetNative]::SetWindowLongPtr($overlayHwnd, [PetNative]::GWL_EXSTYLE, [IntPtr]$overlayStyle) | Out-Null
}
function Set-OverlayClickThroughState($overlayWindow, [bool]$enabled) {
$overlayHwnd = (New-Object Windows.Interop.WindowInteropHelper($overlayWindow)).Handle
$overlayStyle = [PetNative]::GetWindowLongPtr($overlayHwnd, [PetNative]::GWL_EXSTYLE).ToInt64()
if ($enabled) { $overlayStyle = $overlayStyle -bor [PetNative]::WS_EX_TRANSPARENT }
else { $overlayStyle = $overlayStyle -band (-bnot [PetNative]::WS_EX_TRANSPARENT) }
[PetNative]::SetWindowLongPtr($overlayHwnd, [PetNative]::GWL_EXSTYLE, [IntPtr]$overlayStyle) | Out-Null
}
function New-PlatformHighlight($rect) {
$virtualLeft = [Windows.SystemParameters]::VirtualScreenLeft
$virtualRight = $virtualLeft + [Windows.SystemParameters]::VirtualScreenWidth
$highlightLeft = [Math]::Max($virtualLeft, $rect.Left)
$highlightRight = [Math]::Min($virtualRight, $rect.Right)
$highlightWidth = $highlightRight - $highlightLeft
if ($highlightWidth -lt 40) { return $null }
$highlight = New-Object Windows.Window
$highlight.Title = 'Windowisp Platform Highlight'
$highlight.Width = $highlightWidth; $highlight.Height = 12
$highlight.Left = $highlightLeft; $highlight.Top = $rect.Top - 6
$highlight.WindowStyle = [Windows.WindowStyle]::None; $highlight.AllowsTransparency = $true
$highlight.Background = [Windows.Media.Brushes]::Transparent; $highlight.Topmost = $true
$highlight.ShowInTaskbar = $false; $highlight.ShowActivated = $false
$highlight.ResizeMode = [Windows.ResizeMode]::NoResize
$line = New-Object Windows.Controls.Border
$line.Height = 3; $line.VerticalAlignment = [Windows.VerticalAlignment]::Center
$line.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(175, 72, 220, 255))
$line.CornerRadius = 2
$line.Effect = New-Object Windows.Media.Effects.DropShadowEffect
$line.Effect.Color = [Windows.Media.Color]::FromRgb(45, 210, 255)
$line.Effect.BlurRadius = 9; $line.Effect.ShadowDepth = 0; $line.Effect.Opacity = 0.8
$highlight.Content = $line
$highlight.Show(); Set-OverlayClickThrough $highlight
return $highlight
}
function Update-PlatformHighlights {
if (-not $script:active) {
foreach ($entry in $script:platformHighlights.Values) {
$entry.Visibility = [Windows.Visibility]::Hidden
}
return
}
$desired = @{}
foreach ($rect in $script:platforms) {
$key = "$($rect.Left),$($rect.Top),$($rect.Right),$($rect.Bottom)"
$desired[$key] = $rect
if (-not $script:platformHighlights.ContainsKey($key)) {
$overlay = New-PlatformHighlight $rect
if ($null -ne $overlay) { $script:platformHighlights[$key] = $overlay }
} else {
$script:platformHighlights[$key].Visibility = [Windows.Visibility]::Visible
}
}
foreach ($key in @($script:platformHighlights.Keys)) {
if (-not $desired.ContainsKey($key)) {
$script:platformHighlights[$key].Close()
$script:platformHighlights.Remove($key)
}
}
}
function Remove-PlatformHighlights {
foreach ($entry in @($script:platformHighlights.Values)) { $entry.Close() }
$script:platformHighlights.Clear()
}
function Save-FamiliarSettings {
try{$directory=Split-Path -Parent $script:familiarSettingsPath;if(-not(Test-Path -LiteralPath $directory)){New-Item -ItemType Directory -Path $directory -Force|Out-Null};@{mode=$script:familiarMode}|ConvertTo-Json|Set-Content -LiteralPath $script:familiarSettingsPath -Encoding UTF8}catch{}
}
function Set-FamiliarMode([string]$mode) {
if($mode-notin@('Quiet','Normal','Chatty')){$mode='Normal'}
$script:familiarMode=$mode;Save-FamiliarSettings;Update-Menu
}
function Get-FamiliarPhrase([string]$eventName) {
$phrases=@{
'idle'=@('I am right here.');'user-active'=@('Welcome back!');'codex-working'=@('I am keeping watch.','Working on it...')
'completed'=@('All done!');'needs-input'=@('I need your input.');'permission-required'=@('A permission needs your attention.')
'succeeded'=@('That worked!','Success!');'failed'=@('That did not work.','Something went wrong.')
'returning-user'=@('Welcome back!');'celebrate'=@('We did it!');'wait'=@('I will wait here.');'sleep'=@('Time for a tiny rest.');'greet'=@('Hello!')
}
if(-not$phrases.ContainsKey($eventName)){return $null}
$options=@($phrases[$eventName]);return $options[$script:random.Next(0,$options.Count)]
}
function Ensure-FamiliarBubble {
if($null-ne$script:familiarBubbleWindow){return}
$bubbleWindow=New-Object Windows.Window;$bubbleWindow.Title='Windowisp Familiar Bubble';$bubbleWindow.Width=250;$bubbleWindow.Height=78
$bubbleWindow.WindowStyle='None';$bubbleWindow.AllowsTransparency=$true;$bubbleWindow.Background=[Windows.Media.Brushes]::Transparent
$bubbleWindow.Topmost=$script:alwaysOnTop;$bubbleWindow.ShowInTaskbar=$false;$bubbleWindow.ShowActivated=$false;$bubbleWindow.ResizeMode='NoResize'
$canvas=New-Object Windows.Controls.Canvas
$card=New-Object Windows.Controls.Border;$card.Width=242;$card.MinHeight=58;$card.Padding=[Windows.Thickness]::new(14,9,14,9)
$card.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(244,24,55,82));$card.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(116,201,242));$card.BorderThickness=[Windows.Thickness]::new(2);$card.CornerRadius=[Windows.CornerRadius]::new(15)
$text=New-Object Windows.Controls.TextBlock;$text.Foreground=[Windows.Media.Brushes]::White;$text.FontSize=13;$text.FontWeight='SemiBold';$text.TextWrapping='Wrap';$text.TextAlignment='Center';$card.Child=$text
$tail=New-Object Windows.Shapes.Polygon;$tail.Points=[Windows.Media.PointCollection]::Parse('108,57 130,57 119,74');$tail.Fill=$card.Background;$tail.Stroke=$card.BorderBrush;$tail.StrokeThickness=2
$canvas.Children.Add($tail)|Out-Null;$canvas.Children.Add($card)|Out-Null;$bubbleWindow.Content=$canvas
$bubbleWindow.Show();Set-OverlayClickThrough $bubbleWindow;$bubbleWindow.Hide()
$script:familiarBubbleWindow=$bubbleWindow;$script:familiarBubbleText=$text
}
function Update-FamiliarBubblePosition {
if($null-eq$script:familiarBubbleWindow-or-not$script:familiarBubbleWindow.IsVisible){return}
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$right=$left+[Windows.SystemParameters]::VirtualScreenWidth;$bottom=$top+[Windows.SystemParameters]::VirtualScreenHeight
$desiredX=$script:x+($PetWidth/2)-($script:familiarBubbleWindow.Width/2)
$desiredY=$script:y-$script:familiarBubbleWindow.Height-8
if($desiredY-lt$top+6){$desiredY=$script:y+$PetHeight+8}
$script:familiarBubbleWindow.Left=[Math]::Max($left+6,[Math]::Min($right-$script:familiarBubbleWindow.Width-6,$desiredX))
$script:familiarBubbleWindow.Top=[Math]::Max($top+6,[Math]::Min($bottom-$script:familiarBubbleWindow.Height-6,$desiredY))
}
function Show-FamiliarBubble([string]$eventName) {
$critical=$eventName-in@('needs-input','permission-required','failed')
if($script:familiarMode-eq'Quiet'-and-not$critical){return}
if($script:familiarMode-eq'Normal'-and$eventName-in@('idle','user-active','codex-working','wait')){return}
$now=[DateTime]::UtcNow
if($eventName-eq$script:familiarLastEvent-and($now-$script:familiarLastEventAt).TotalSeconds-lt8){return}
if(($now-$script:familiarLastEventAt).TotalSeconds-lt1){return}
$phrase=Get-FamiliarPhrase $eventName;if([string]::IsNullOrWhiteSpace($phrase)){return}
Ensure-FamiliarBubble;$script:familiarBubbleText.Text=$phrase;$script:familiarBubblePersistent=$eventName-in@('needs-input','permission-required')
$script:familiarBubbleTicks=$(if($script:familiarBubblePersistent){-1}else{360});$script:familiarLastEvent=$eventName;$script:familiarLastEventAt=$now
$script:familiarBubbleWindow.Topmost=$script:alwaysOnTop;$script:familiarBubbleWindow.Show();Update-FamiliarBubblePosition
}
function Receive-FamiliarEvent {
$path=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Windowisp\familiar-event.json'
if(-not(Test-Path -LiteralPath $path)){return}
try{$request=Get-Content -LiteralPath $path -Raw|ConvertFrom-Json;$eventName=[string]$request.event;if($eventName-in@('idle','user-active','codex-working','completed','needs-input','permission-required','succeeded','failed','returning-user','celebrate','wait','sleep','greet')){Show-FamiliarBubble $eventName}}catch{}
}
function Update-FamiliarBubble {
if($null-eq$script:familiarBubbleWindow-or-not$script:familiarBubbleWindow.IsVisible){return}
Update-FamiliarBubblePosition
if(-not$script:familiarBubblePersistent-and$script:familiarBubbleTicks-gt0){$script:familiarBubbleTicks--;if($script:familiarBubbleTicks-le0){$script:familiarBubbleWindow.Hide()}}
}
function New-MenuButton([string]$text, [scriptblock]$action, [string]$shortcut = '') {
$button = New-Object Windows.Controls.Button
$button.Height = 36; $button.Margin = [Windows.Thickness]::new(8, 3, 8, 3)
# Gameplay uses raw keyboard state. Menu and hotbar buttons are mouse-only so
# Space/WASD can never activate or navigate a control left focused behind play.
$button.Focusable = $false
$button.IsTabStop = $false
$button.HorizontalContentAlignment = [Windows.HorizontalAlignment]::Stretch
$button.Background = New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(34, 70, 101),[Windows.Media.Color]::FromRgb(21, 43, 67),90)
$button.Foreground = [Windows.Media.Brushes]::White
$button.BorderBrush = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(104, 168, 211))
$button.BorderThickness = [Windows.Thickness]::new(1)
$button.Cursor = [Windows.Input.Cursors]::Hand
$content = New-Object Windows.Controls.Grid
$label = New-Object Windows.Controls.TextBlock
$label.Text = $text; $label.FontSize = 14; $label.Margin = [Windows.Thickness]::new(8, 0, 8, 0)
$label.VerticalAlignment = [Windows.VerticalAlignment]::Center
$hint = New-Object Windows.Controls.TextBlock
$hint.Text = $shortcut; $hint.FontSize = 12; $hint.Opacity = 0.68
$hint.HorizontalAlignment = [Windows.HorizontalAlignment]::Right
$hint.VerticalAlignment = [Windows.VerticalAlignment]::Center
$hint.Margin = [Windows.Thickness]::new(8, 0, 8, 0)
$content.Children.Add($label) | Out-Null; $content.Children.Add($hint) | Out-Null
$button.Content = $content
$button.Add_Click($action)
return $button
}
function Reset-PetPosition {
$script:x = [Windows.SystemParameters]::VirtualScreenLeft + 80
$script:y = [Windows.SystemParameters]::VirtualScreenTop + 80
$script:vx = 0; $script:vy = 0; $script:grounded = $false
$script:groundPlatformHwnd = [IntPtr]::Zero; $script:groundPlatformRect = $null
$script:wallContact = 0; $script:wallGraceTicks = 0
$script:lastRenderKey = ''
}
function Reset-WindowispRuntime {
# Clear transient interaction state without deleting the player's sandbox.
try { [Windows.Input.Mouse]::Capture($null) | Out-Null } catch {}
Close-FreeRotationHandle
Set-EraserMode $false
Set-RopeToolMode $false
Set-WiringMode $false
$script:capturingBinding = $null
$script:previousKeys = @{ Jump=$false; Drop=$false; Attack=$false; Interact=$false; Left=$false; Right=$false }
$script:p2Previous = @{ Jump=$false; Drop=$false; Attack=$false; Interact=$false }
$script:charging = $false; $script:chargeTicks = 0; $script:attackTicks = 0; $script:attackCooldown = 0
$script:dropTicks = 0; $script:dashTicks = 0; $script:dashCooldown = 0
$script:coyoteTicks = 0; $script:airJumpsRemaining = 1
$script:p2AttackTicks = 0; $script:p2AttackCooldown = 0; $script:p2DropTicks = 0
$script:p2WallContact = 0; $script:p2WallGraceTicks = 0; $script:p2AirJumps = 1
foreach ($placement in @($script:playroomBlocks) + @($script:playroomBalls) + @($script:playroomGadgets)) {
if ($null -ne $placement.PSObject.Properties['Dragging']) { $placement.Dragging = $false }
if ($null -ne $placement.PSObject.Properties['CarriedBy'] -and $placement.CarriedBy) {
$placement.CarriedBy = ''; $placement.VX = 0.0; $placement.VY = 2.0
}
}
Reset-PetPosition
if ($script:twoPlayerActive) {
$script:p2X = $script:x + 110; $script:p2Y = $script:y
$script:p2VX = 0.0; $script:p2VY = 0.0; $script:p2Grounded = $false
$script:p2GroundPlatformHwnd = [IntPtr]::Zero; $script:p2GroundPlatformRect = $null
}
$window.Opacity = 1.0
if ($null -ne $script:player2Window) { $script:player2Window.Opacity = 1.0 }
Set-GameMode $false
Set-Frozen $false
Set-GameMode $true
[Windows.Input.Keyboard]::ClearFocus()
Update-Hud; Update-Menu
}
function Restart-WindowispRuntime {
$launcher = Join-Path $PSScriptRoot 'game-mode.ps1'
$action = $(if ($script:playroomActive) { 'Playroom' } else { 'On' })
$escapedLauncher = $launcher.Replace("'", "''")
$restartCommand = "Start-Sleep -Milliseconds 900; & '$escapedLauncher' -Action $action"
$encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($restartCommand))
Start-Process powershell.exe -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-EncodedCommand',$encodedCommand) -WindowStyle Hidden | Out-Null
$window.Close()
}
$toyboxAssetRoot = Join-Path $PSScriptRoot 'assets\toybox-v1'
$toyboxAssetFiles = @{
Normal='block-normal.png'; Bouncy='block-bouncy.png'; Fire='block-fire.png'
Ice='block-ice.png'; Mirror='block-mirror.png'; Ball='toy-ball.png'
NormalTile='block-normal-tile.png'; Wooden='block-wooden-tile.png'; WoodenTile='block-wooden-tile.png'
Conveyor='block-conveyor-tile.png'; ConveyorTile='block-conveyor-tile.png'; BouncyTile='block-bouncy-tile.png'
FireTile='block-fire-tile.png'; IceTile='block-ice-tile.png'; GrassTile='block-grass-tile.png'; MirrorTile='block-mirror-tile.png'
NormalMaterial='material-normal-v2.png'; BouncyMaterial='material-bouncy-v2.png'; FireMaterial='material-fire-v2.png'; IceMaterial='material-ice-v2.png'; GrassMaterial='material-grass-dirt-v2.png'
CloudMaterial='material-cloud-v1.png'; CrumblingMaterial='material-crumbling-v1.png'; WispfallLava='material-wispfall-lava-v1.png'
Bat='toy-bat.png'; Bone='toy-bone.png'; Spring='gadget-spring.png'
Teleporter='gadget-teleporter.png'; TeleporterA='gadget-teleporter.png'
TeleporterB='gadget-teleporter.png'; Fan='gadget-fan-v2.png'; Rotator='gadget-rotator.png'; RopeCoil='gadget-rope-coil.png'
Switch='gadget-switch.png'; Button='gadget-button.png'; PressurePlate='gadget-pressureplate.png'; Timer='gadget-timer.png'
WiringSpool='gadget-wiring-spool.png'; WireSegment='gadget-wire-segment.png'
Spikes='hazard-spikes.png'; FireJet='hazard-firejet.png'; Laser='hazard-laser.png'
ShieldPickup='pickup-shield-v2.png'; StarPickup='pickup-star-v2.png'
ChestCommon='treasure-chest-common-v1.png'; ChestRare='treasure-chest-rare-v1.png'; ChestLegendary='treasure-chest-legendary-v1.png'
ChestCommonOpen='treasure-chest-common-open-v1.png'; ChestRareOpen='treasure-chest-rare-open-v1.png'; ChestLegendaryOpen='treasure-chest-legendary-open-v1.png'
}
# Future game modes can roll a rarity first, then resolve artwork and contents
# independently. No chest is spawned by Survival yet.
$treasureChestRarities=[ordered]@{
Common=[pscustomobject]@{ClosedAsset='ChestCommon';OpenAsset='ChestCommonOpen';Weight=70;RewardTier=1;Colour='#7890A6';OpenTicks=16;CloseTicks=12;Lift=3;BurstParticles=3;EffectScale=.55}
Rare=[pscustomobject]@{ClosedAsset='ChestRare';OpenAsset='ChestRareOpen';Weight=25;RewardTier=2;Colour='#42C9F5';OpenTicks=19;CloseTicks=14;Lift=5;BurstParticles=7;EffectScale=1.0}
Legendary=[pscustomobject]@{ClosedAsset='ChestLegendary';OpenAsset='ChestLegendaryOpen';Weight=5;RewardTier=3;Colour='#F6B83F';OpenTicks=23;CloseTicks=16;Lift=8;BurstParticles=12;EffectScale=1.65}
}
function Get-ToyboxImageSource([string]$kind) {
if(-not$script:toyboxAssetFiles.ContainsKey($kind)){return $null}
$path=Join-Path $script:toyboxAssetRoot $script:toyboxAssetFiles[$kind]
if(-not(Test-Path -LiteralPath $path)){return $null}
$bitmap=New-Object Windows.Media.Imaging.BitmapImage
$bitmap.BeginInit()
$bitmap.CacheOption=[Windows.Media.Imaging.BitmapCacheOption]::OnLoad
$bitmap.UriSource=[Uri]::new((Resolve-Path -LiteralPath $path).Path)
$bitmap.EndInit()
$bitmap.Freeze()
return $bitmap
}
function New-ToyboxImage([string]$kind) {
$source=Get-ToyboxImageSource $kind
if($null-eq$source){return $null}
$image=New-Object Windows.Controls.Image
$image.Source=$source
$image.Stretch=[Windows.Media.Stretch]::Uniform
$image.SnapsToDevicePixels=$true
$image.IsHitTestVisible=$false
return $image
}
function Get-BlockBrush([string]$type) {
if($type-eq'Mirror'){
$mirrorBrush=New-Object Windows.Media.LinearGradientBrush
$mirrorBrush.StartPoint=[Windows.Point]::new(0,0);$mirrorBrush.EndPoint=[Windows.Point]::new(1,1)
$mirrorBrush.GradientStops.Add((New-Object Windows.Media.GradientStop ([Windows.Media.Color]::FromRgb(225,250,255),0.0)))
$mirrorBrush.GradientStops.Add((New-Object Windows.Media.GradientStop ([Windows.Media.Color]::FromRgb(104,194,232),0.24)))
$mirrorBrush.GradientStops.Add((New-Object Windows.Media.GradientStop ([Windows.Media.Color]::FromRgb(245,253,255),0.36)))
$mirrorBrush.GradientStops.Add((New-Object Windows.Media.GradientStop ([Windows.Media.Color]::FromRgb(61,117,172),0.58)))
$mirrorBrush.GradientStops.Add((New-Object Windows.Media.GradientStop ([Windows.Media.Color]::FromRgb(189,239,250),0.78)))
$mirrorBrush.GradientStops.Add((New-Object Windows.Media.GradientStop ([Windows.Media.Color]::FromRgb(48,85,135),1.0)))
return $mirrorBrush
}
$materialSource=Get-ToyboxImageSource "$($type)Material"
if($null-ne$materialSource){
$materialBrush=New-Object Windows.Media.ImageBrush $materialSource
$materialBrush.TileMode=[Windows.Media.TileMode]::FlipXY
$materialBrush.ViewportUnits=[Windows.Media.BrushMappingMode]::Absolute
$materialSize=$(if($type-eq'Normal'){176}elseif($type-in@('Bouncy','Fire')){210}elseif($type-eq'Ice'){230}elseif($type-eq'Cloud'){330}elseif($type-eq'Crumbling'){260}else{280})
$materialBrush.Viewport=[Windows.Rect]::new(0,0,$materialSize,$materialSize)
$materialBrush.ViewboxUnits=[Windows.Media.BrushMappingMode]::RelativeToBoundingBox;$materialBrush.Viewbox=[Windows.Rect]::new(0,0,1,1)
$materialBrush.Stretch=[Windows.Media.Stretch]::Fill;$materialBrush.AlignmentX='Left';$materialBrush.AlignmentY='Top'
return $materialBrush
}
$source=Get-ToyboxImageSource "$($type)Tile"
if($null-ne$source){
$artBrush=New-Object Windows.Media.ImageBrush $source
$singleSurface=$type-in@('Bouncy','Fire','Ice','Mirror')
$artBrush.TileMode=$(if($type-in@('Normal','Grass')){[Windows.Media.TileMode]::FlipXY}elseif($singleSurface){[Windows.Media.TileMode]::None}else{[Windows.Media.TileMode]::Tile})
$artBrush.ViewportUnits=$(if($singleSurface){[Windows.Media.BrushMappingMode]::RelativeToBoundingBox}else{[Windows.Media.BrushMappingMode]::Absolute})
$artBrush.Viewport=$(if($singleSurface){[Windows.Rect]::new(0,0,1,1)}elseif($type-eq'Grass'){[Windows.Rect]::new(0,0,320,96)}elseif($type-eq'Conveyor'){[Windows.Rect]::new(0,0,58,32)}else{[Windows.Rect]::new(0,0,72,34)})
# The generated tile PNGs are complete rounded platform silhouettes.
# Repeat only their filled interior so transparent outer gutters and
# pointed end-caps do not turn a resized block into separate mini-blocks.
$artBrush.ViewboxUnits=[Windows.Media.BrushMappingMode]::RelativeToBoundingBox
$artBrush.Viewbox=$(if($type-eq'Conveyor'){[Windows.Rect]::new(.36,.11,.28,.78)}elseif($type-eq'Grass'){[Windows.Rect]::new(.08,.54,.84,.34)}else{[Windows.Rect]::new(.09,.11,.82,.78)})
$artBrush.Stretch=[Windows.Media.Stretch]::Fill
$artBrush.AlignmentX=[Windows.Media.AlignmentX]::Left
$artBrush.AlignmentY=[Windows.Media.AlignmentY]::Top
return $artBrush
}
$xaml = switch ($type) {
'Bouncy' {
'<DrawingBrush xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" TileMode="Tile" Viewport="0,0,52,34" ViewportUnits="Absolute" Viewbox="0,0,52,34" ViewboxUnits="Absolute"><DrawingBrush.Drawing><DrawingGroup><GeometryDrawing Geometry="M0,0 H52 V34 H0 Z"><GeometryDrawing.Brush><LinearGradientBrush StartPoint="0,0" EndPoint="0,1"><GradientStop Color="#FFFF8BCB" Offset="0"/><GradientStop Color="#FFE94399" Offset="0.52"/><GradientStop Color="#FF9E247D" Offset="1"/></LinearGradientBrush></GeometryDrawing.Brush></GeometryDrawing><GeometryDrawing Geometry="M5,25 C9,7 20,4 26,17 C32,4 43,7 47,25"><GeometryDrawing.Pen><Pen Brush="#99FFFFFF" Thickness="2"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M10,8 C15,4 19,4 22,7"><GeometryDrawing.Pen><Pen Brush="#CCFFFFFF" Thickness="2.4" StartLineCap="Round" EndLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing></DrawingGroup></DrawingBrush.Drawing></DrawingBrush>'
}
'Fire' {
'<DrawingBrush xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" TileMode="Tile" Viewport="0,0,64,34" ViewportUnits="Absolute" Viewbox="0,0,64,34" ViewboxUnits="Absolute"><DrawingBrush.Drawing><DrawingGroup><GeometryDrawing Geometry="M0,0 H64 V34 H0 Z"><GeometryDrawing.Brush><LinearGradientBrush StartPoint="0,0" EndPoint="0,1"><GradientStop Color="#FFFFC928" Offset="0"/><GradientStop Color="#FFFF5A1F" Offset="0.5"/><GradientStop Color="#FF9D1524" Offset="1"/></LinearGradientBrush></GeometryDrawing.Brush></GeometryDrawing><GeometryDrawing Geometry="M0,23 L10,15 L18,25 L30,8 L41,22 L51,12 L64,22 V34 H0 Z" Brush="#FFD92720"/><GeometryDrawing Geometry="M0,28 L12,20 L20,29 L30,16 L42,27 L53,19 L64,27"><GeometryDrawing.Pen><Pen Brush="#FFFFE45B" Thickness="2.4" StartLineCap="Round" EndLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing></DrawingGroup></DrawingBrush.Drawing></DrawingBrush>'
}
'Ice' {
'<DrawingBrush xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" TileMode="Tile" Viewport="0,0,64,34" ViewportUnits="Absolute" Viewbox="0,0,64,34" ViewboxUnits="Absolute"><DrawingBrush.Drawing><DrawingGroup><GeometryDrawing Geometry="M0,0 H64 V34 H0 Z"><GeometryDrawing.Brush><LinearGradientBrush StartPoint="0,0" EndPoint="0,1"><GradientStop Color="#FFE9FCFF" Offset="0"/><GradientStop Color="#FF71DDF5" Offset="0.55"/><GradientStop Color="#FF218BBE" Offset="1"/></LinearGradientBrush></GeometryDrawing.Brush></GeometryDrawing><GeometryDrawing Geometry="M0,0 L18,0 L8,34 L0,34 Z" Brush="#668CFAFF"/><GeometryDrawing Geometry="M18,0 L42,0 L31,34 L8,34 Z" Brush="#55FFFFFF"/><GeometryDrawing Geometry="M42,0 L64,0 L64,34 L31,34 Z" Brush="#554BBDE8"/><GeometryDrawing Geometry="M18,0 L8,34 M42,0 L31,34"><GeometryDrawing.Pen><Pen Brush="#AAFFFFFF" Thickness="1.2"/></GeometryDrawing.Pen></GeometryDrawing></DrawingGroup></DrawingBrush.Drawing></DrawingBrush>'
}
'Grass' {
'<DrawingBrush xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" TileMode="Tile" Viewport="0,0,64,34" ViewportUnits="Absolute" Viewbox="0,0,64,34" ViewboxUnits="Absolute"><DrawingBrush.Drawing><DrawingGroup><GeometryDrawing Geometry="M0,0 H64 V34 H0 Z"><GeometryDrawing.Brush><LinearGradientBrush StartPoint="0,0" EndPoint="0,1"><GradientStop Color="#FF8B552B" Offset="0"/><GradientStop Color="#FF61361F" Offset="0.62"/><GradientStop Color="#FF3E241B" Offset="1"/></LinearGradientBrush></GeometryDrawing.Brush></GeometryDrawing><GeometryDrawing Geometry="M0,0 H64 V8 L59,12 L53,9 L47,14 L40,9 L34,13 L27,9 L21,14 L15,9 L9,12 L4,8 L0,11 Z"><GeometryDrawing.Brush><LinearGradientBrush StartPoint="0,0" EndPoint="0,1"><GradientStop Color="#FFC2ED72" Offset="0"/><GradientStop Color="#FF70C64F" Offset="0.48"/><GradientStop Color="#FF31823C" Offset="1"/></LinearGradientBrush></GeometryDrawing.Brush></GeometryDrawing><GeometryDrawing Geometry="M0,1 H64"><GeometryDrawing.Pen><Pen Brush="#FFE1F99B" Thickness="2"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M0,10 L5,7 L9,12 L15,9 L21,14 L27,9 L34,13 L40,9 L47,14 L53,9 L59,12 L64,8"><GeometryDrawing.Pen><Pen Brush="#FF285F35" Thickness="1.5"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M7,4 L9,0 M15,6 L17,1 M29,5 L31,0 M43,6 L46,1 M56,5 L58,0"><GeometryDrawing.Pen><Pen Brush="#FFD5F58A" Thickness="1.4" StartLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M12,17 L17,22 L15,29 M38,16 L34,22 L37,28 M55,15 L51,20"><GeometryDrawing.Pen><Pen Brush="#FFC38A50" Thickness="1.5" StartLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M5,25 L10,22 L14,25 L10,28 Z" Brush="#FF9A7048"/><GeometryDrawing Geometry="M45,25 L50,22 L55,25 L52,29 L47,29 Z" Brush="#FF3F6B69"/><GeometryDrawing Geometry="M0,33 H64"><GeometryDrawing.Pen><Pen Brush="#FF2B1B17" Thickness="2"/></GeometryDrawing.Pen></GeometryDrawing></DrawingGroup></DrawingBrush.Drawing></DrawingBrush>'
}
'Cloud' {
'<DrawingBrush xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" TileMode="Tile" Viewport="0,0,240,104" ViewportUnits="Absolute" Viewbox="0,0,240,104" ViewboxUnits="Absolute"><DrawingBrush.Drawing><DrawingGroup><GeometryDrawing Geometry="M0,0 H240 V104 H0 Z"><GeometryDrawing.Brush><LinearGradientBrush StartPoint="0,0" EndPoint="0,1"><GradientStop Color="#FF72CFF4" Offset="0"/><GradientStop Color="#FFBDEBFA" Offset="1"/></LinearGradientBrush></GeometryDrawing.Brush></GeometryDrawing><GeometryDrawing Geometry="M-12,37 C-9,24 2,20 14,24 C18,9 37,5 48,19 C57,10 75,15 77,29 C91,26 102,35 99,47 H-5 Z M116,28 C119,18 128,15 138,18 C143,4 161,2 169,16 C180,8 194,15 195,27 C210,24 220,33 217,44 H111 Z M48,84 C48,70 60,64 72,68 C78,49 101,45 113,62 C123,50 143,55 146,70 C160,67 173,77 169,91 H42 Z M176,92 C174,81 183,73 194,75 C198,61 215,58 224,71 C234,66 245,74 247,87 V103 H177 Z"><GeometryDrawing.Brush><LinearGradientBrush StartPoint="0,0" EndPoint="0,1"><GradientStop Color="#FFFFFFFF" Offset="0"/><GradientStop Color="#FFE5F8FF" Offset=".45"/><GradientStop Color="#FF92CFE8" Offset="1"/></LinearGradientBrush></GeometryDrawing.Brush><GeometryDrawing.Pen><Pen Brush="#EEFFFFFF" Thickness="2.4"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M7,37 C30,27 65,29 88,39 M126,31 C149,22 188,24 207,35 M58,82 C84,68 132,69 158,84 M190,90 C208,81 229,82 242,92"><GeometryDrawing.Pen><Pen Brush="#77FFFFFF" Thickness="3" StartLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M0,101 C35,95 53,99 78,96 C112,91 143,102 177,96 C202,92 220,98 240,94 V104 H0 Z" Brush="#5586C7E4"/></DrawingGroup></DrawingBrush.Drawing></DrawingBrush>'
}
'Crumbling' {
'<DrawingBrush xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" TileMode="Tile" Viewport="0,0,180,82" ViewportUnits="Absolute" Viewbox="0,0,180,82" ViewboxUnits="Absolute"><DrawingBrush.Drawing><DrawingGroup><GeometryDrawing Geometry="M0,0 H180 V82 H0 Z"><GeometryDrawing.Brush><LinearGradientBrush StartPoint="0,0" EndPoint="0,1"><GradientStop Color="#FFC8874B" Offset="0"/><GradientStop Color="#FF87502F" Offset=".5"/><GradientStop Color="#FF4A2B22" Offset="1"/></LinearGradientBrush></GeometryDrawing.Brush></GeometryDrawing><GeometryDrawing Geometry="M0,0 H180 V8 C165,5 151,11 136,7 C119,3 103,12 87,7 C68,2 55,11 37,7 C23,4 12,10 0,6 Z" Brush="#FFE7B86C"/><GeometryDrawing Geometry="M0,8 C28,13 49,5 74,10 C100,15 123,5 148,10 C160,13 171,8 180,10"><GeometryDrawing.Pen><Pen Brush="#FF633722" Thickness="2"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M24,8 L31,19 L27,31 L39,41 L34,57 L44,68 M111,0 L106,13 L115,25 L108,39 L121,52 L116,70 L123,82 M158,9 L150,22 L156,34 L148,48"><GeometryDrawing.Pen><Pen Brush="#FF3E211C" Thickness="2.5" StartLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M13,27 L20,23 L27,28 L21,33 L14,32 Z M66,54 L74,48 L83,53 L79,61 L69,62 Z M139,24 L146,20 L153,25 L149,31 L141,31 Z" Brush="#FFB2946C"><GeometryDrawing.Pen><Pen Brush="#FF4A3529" Thickness="1.3"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M52,16 C56,25 49,31 53,41 M92,42 C83,47 87,59 79,66 M163,49 C154,57 161,66 152,75"><GeometryDrawing.Pen><Pen Brush="#FFB67A42" Thickness="2" StartLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M126,65 C131,59 140,61 141,67 C147,64 152,69 149,74 C143,78 131,76 126,71 Z M124,68 L119,65 M150,71 L156,74"><GeometryDrawing.Pen><Pen Brush="#FFE7D6AE" Thickness="2.5" StartLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M0,80 H180"><GeometryDrawing.Pen><Pen Brush="#FF2C1918" Thickness="3"/></GeometryDrawing.Pen></GeometryDrawing></DrawingGroup></DrawingBrush.Drawing></DrawingBrush>'
}
'Ladder' {
'<DrawingBrush xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" TileMode="Tile" Viewport="0,0,56,40" ViewportUnits="Absolute" Viewbox="0,0,56,40" ViewboxUnits="Absolute"><DrawingBrush.Drawing><DrawingGroup><GeometryDrawing Geometry="M0,0 H56 V40 H0 Z" Brush="#4423150C"/><GeometryDrawing Geometry="M7,-2 V42 M49,-2 V42"><GeometryDrawing.Pen><Pen Brush="#FF6F371B" Thickness="10" StartLineCap="Round" EndLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M7,-2 V42 M49,-2 V42"><GeometryDrawing.Pen><Pen Brush="#FFD7964D" Thickness="5" StartLineCap="Round" EndLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M8,10 H48 M8,32 H48"><GeometryDrawing.Pen><Pen Brush="#FFB86D31" Thickness="8" StartLineCap="Round" EndLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M10,8 H46 M10,30 H46"><GeometryDrawing.Pen><Pen Brush="#FFFFC875" Thickness="2.2" StartLineCap="Round" EndLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing></DrawingGroup></DrawingBrush.Drawing></DrawingBrush>'
}
'Conveyor' {
'<DrawingBrush xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" TileMode="Tile" Viewport="0,0,58,32" ViewportUnits="Absolute" Viewbox="0,0,58,32" ViewboxUnits="Absolute"><DrawingBrush.Drawing><DrawingGroup><GeometryDrawing Geometry="M0,0 H58 V32 H0 Z"><GeometryDrawing.Brush><LinearGradientBrush StartPoint="0,0" EndPoint="0,1"><GradientStop Color="#FF495467" Offset="0"/><GradientStop Color="#FF202735" Offset="0.48"/><GradientStop Color="#FF111721" Offset="1"/></LinearGradientBrush></GeometryDrawing.Brush></GeometryDrawing><GeometryDrawing Geometry="M0,3 H58 M0,27 H58"><GeometryDrawing.Pen><Pen Brush="#FFA9C3D8" Thickness="2"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M7,16 A7,7 0 1 0 21,16 A7,7 0 1 0 7,16 M37,16 A7,7 0 1 0 51,16 A7,7 0 1 0 37,16"><GeometryDrawing.Pen><Pen Brush="#FF8296AA" Thickness="3"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M11,16 H47"><GeometryDrawing.Pen><Pen Brush="#FF61D4E8" Thickness="2" StartLineCap="Round" EndLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing></DrawingGroup></DrawingBrush.Drawing></DrawingBrush>'
}
'Mirror' {
'<DrawingBrush xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" TileMode="Tile" Viewport="0,0,64,32" ViewportUnits="Absolute" Viewbox="0,0,64,32" ViewboxUnits="Absolute"><DrawingBrush.Drawing><DrawingGroup><GeometryDrawing Geometry="M0,0 H64 V32 H0 Z"><GeometryDrawing.Brush><LinearGradientBrush StartPoint="0,0" EndPoint="1,1"><GradientStop Color="#FFF8FFFF" Offset="0"/><GradientStop Color="#FF85D7EF" Offset="0.45"/><GradientStop Color="#FF376DA5" Offset="1"/></LinearGradientBrush></GeometryDrawing.Brush></GeometryDrawing><GeometryDrawing Geometry="M7,27 L29,5 M34,27 L56,5"><GeometryDrawing.Pen><Pen Brush="#CCFFFFFF" Thickness="3" StartLineCap="Round" EndLineCap="Round"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M0,1 H64 V31 H0 Z"><GeometryDrawing.Pen><Pen Brush="#FFE5F7FF" Thickness="2"/></GeometryDrawing.Pen></GeometryDrawing></DrawingGroup></DrawingBrush.Drawing></DrawingBrush>'
}
default {
'<DrawingBrush xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" TileMode="Tile" Viewport="0,0,64,32" ViewportUnits="Absolute" Viewbox="0,0,64,32" ViewboxUnits="Absolute"><DrawingBrush.Drawing><DrawingGroup><GeometryDrawing Geometry="M0,0 H64 V32 H0 Z" Brush="#FF626777"/><GeometryDrawing Geometry="M0,0 H64 M0,16 H64 M0,32 H64 M16,0 V16 M48,16 V32"><GeometryDrawing.Pen><Pen Brush="#FFD8D1C4" Thickness="2"/></GeometryDrawing.Pen></GeometryDrawing><GeometryDrawing Geometry="M1,2 H63 M1,18 H63"><GeometryDrawing.Pen><Pen Brush="#557A8294" Thickness="2"/></GeometryDrawing.Pen></GeometryDrawing></DrawingGroup></DrawingBrush.Drawing></DrawingBrush>'
}
}
return [Windows.Markup.XamlReader]::Parse($xaml)
}
function Set-BlockGameplayVisual($blockData, [bool]$gameplay) {
$blockData.Label.Visibility = $(if ($gameplay) { [Windows.Visibility]::Collapsed } else { [Windows.Visibility]::Visible })
foreach ($handle in @($blockData.Handles)) {
$handle.Visibility = $(if ($gameplay) { [Windows.Visibility]::Collapsed } else { [Windows.Visibility]::Visible })
}
$blockData.Element.Cursor = $(if ($gameplay) { [Windows.Input.Cursors]::Arrow } else { [Windows.Input.Cursors]::SizeAll })
$blockData.Element.BorderThickness = [Windows.Thickness]::new($(if ($gameplay) { 1.25 } else { 2 }))
Set-OverlayClickThroughState $blockData.Window $gameplay
}
function Set-BlockTint($blockData, [int]$tintIndex) {
$colors = @(
[Windows.Media.Color]::FromArgb(0,255,255,255),
[Windows.Media.Color]::FromArgb(82,152,84,255),
[Windows.Media.Color]::FromArgb(82,30,132,255),
[Windows.Media.Color]::FromArgb(82,44,190,103),
[Windows.Media.Color]::FromArgb(82,255,178,42),
[Windows.Media.Color]::FromArgb(82,255,67,128)
)
$safeIndex=[Math]::Max(0,[Math]::Min($colors.Count-1,$tintIndex))
$blockData.TintIndex=$safeIndex
$blockData.Tint.Background=New-Object Windows.Media.SolidColorBrush $colors[$safeIndex]
}
function Remove-PlayroomBlock($blockData) {
if ($null -eq $blockData) { return }
if($script:activeRotationData-eq$blockData){Close-FreeRotationHandle}
Remove-RopesForPlacement $blockData
$script:playroomBlocks.Remove($blockData)
$blockData.Window.Close()
}
function Trigger-TemporaryBlock($blockData,[string]$sourceKey='Player1') {
if($null-eq$blockData-or$null-eq$blockData.PSObject.Properties['Type']-or$null-eq$blockData.PSObject.Properties['TemporaryState']){return}
if($blockData.TemporaryState-ne'Stable'-or$blockData.Type-notin@('Cloud','Crumbling')){return}
if([string]::IsNullOrWhiteSpace($sourceKey)){$sourceKey='Unknown'}
$contactTick=[int]$script:hazardTick
$lastContact=$(if($blockData.ContactTicks.ContainsKey($sourceKey)){[int]$blockData.ContactTicks[$sourceKey]}else{-999999})
$newLanding=($contactTick-$lastContact)-gt8
$blockData.ContactTicks[$sourceKey]=$contactTick
if(-not$newLanding){return}
if($blockData.Type-eq'Cloud'){
$blockData.TemporaryState='Warning';$blockData.TemporaryTicks=75;$blockData.Label.Text='PUFF!'
}elseif($blockData.Type-eq'Crumbling'){
$blockData.Landings++
$blockData.CrumbleShakeTicks=12
$blockData.CrumbleOverlay.Opacity=[Math]::Min(1,.24+($blockData.Landings*.25))
$blockData.Label.Text="CRACK $($blockData.Landings)/3"
if($blockData.Landings-ge3){$blockData.TemporaryState='Warning';$blockData.TemporaryTicks=35}
}
}
function Update-TemporaryBlocks {
foreach($blockData in @($script:playroomBlocks|Where-Object{$_.Type-in@('Cloud','Crumbling')})){
if($blockData.Type-eq'Crumbling'){
if($blockData.CrumbleShakeTicks-gt0){
$blockData.CrumbleShakeTicks--
$blockData.CrumbleTranslate.X=$(if(($blockData.CrumbleShakeTicks%2)-eq0){-2.4}else{2.4})
$blockData.CrumbleTranslate.Y=$(if(($blockData.CrumbleShakeTicks%3)-eq0){1.5}else{0})
$pieceIndex=0
foreach($piece in @($blockData.CrumblePieces)){
$piece.Opacity=[Math]::Min(1,.25+($blockData.Landings*.22))
[Windows.Controls.Canvas]::SetTop($piece,4+(($pieceIndex*7+$blockData.CrumbleShakeTicks*2)%22));$pieceIndex++
}
}else{$blockData.CrumbleTranslate.X=0;$blockData.CrumbleTranslate.Y=0}
}
if($blockData.TemporaryState-eq'Warning'){
$blockData.TemporaryTicks--
if($blockData.Type-eq'Crumbling'){
$collapse=[Math]::Max(.05,$blockData.TemporaryTicks/35.0)
$blockData.CrumbleScale.ScaleY=$collapse;$blockData.CrumbleTranslate.Y=(1-$collapse)*$blockData.Height
$blockData.Element.Opacity=[Math]::Max(.18,$collapse)
}else{$blockData.Element.Opacity=$(if(($blockData.TemporaryTicks%12)-lt6){.3}else{.82})}
if($blockData.TemporaryTicks-le0){
$blockData.TemporaryState='Gone';$blockData.TemporaryTicks=$(if($script:wispfallActive){999999}else{240})
$blockData.Window.Hide()
}
}elseif($blockData.TemporaryState-eq'Gone'-and-not$script:wispfallActive){
$blockData.TemporaryTicks--
if($blockData.TemporaryTicks-le0){
$blockData.TemporaryState='Stable';$blockData.Landings=0;$blockData.Element.Opacity=1;$blockData.Label.Text=$blockData.Type.ToUpperInvariant();$blockData.ContactTicks.Clear()
if($blockData.Type-eq'Crumbling'){$blockData.CrumbleScale.ScaleY=1;$blockData.CrumbleTranslate.X=0;$blockData.CrumbleTranslate.Y=0;$blockData.CrumbleOverlay.Opacity=.12}
$blockData.Window.Show()
}
}
}
}
function Get-GrassCapBrush {
$source=Get-ToyboxImageSource 'GrassTile'
if($null-eq$source){return New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(83,170,66))}
$brush=New-Object Windows.Media.ImageBrush $source
$brush.TileMode=[Windows.Media.TileMode]::Tile;$brush.ViewportUnits=[Windows.Media.BrushMappingMode]::Absolute;$brush.Viewport=[Windows.Rect]::new(0,0,96,14)
$brush.ViewboxUnits=[Windows.Media.BrushMappingMode]::RelativeToBoundingBox;$brush.Viewbox=[Windows.Rect]::new(.08,.08,.84,.48)
$brush.Stretch=[Windows.Media.Stretch]::Fill;$brush.AlignmentX='Left';$brush.AlignmentY='Top'
return $brush
}
function Set-ConveyorDirection($blockData,[int]$direction) {
if($null-eq$blockData-or$blockData.Type-ne'Conveyor'){return}
$blockData.Direction=$(if($direction-lt0){-1}else{1})
if($null-ne$blockData.ConveyorArrows){$blockData.ConveyorArrows.Text=$(if($blockData.Direction-lt0){'< < < <'}else{'> > > >'})}
$blockData.Label.Text=$(if($blockData.Direction-lt0){'CONVEYOR <'}else{'CONVEYOR >'})
}
function Update-ConveyorVisuals {
foreach($conveyor in @($script:playroomBlocks|Where-Object{$_.Type-eq'Conveyor'})){
$conveyor.VisualPhase=($conveyor.VisualPhase+($conveyor.Direction*.75))%58
if($conveyor.VisualPhase-lt0){$conveyor.VisualPhase+=58}
if($null-ne$conveyor.ConveyorBeltTransform){$conveyor.ConveyorBeltTransform.X=$conveyor.VisualPhase}
if($null-ne$conveyor.ConveyorArrowTransform){$conveyor.ConveyorArrowTransform.X=($conveyor.VisualPhase%29)-14.5}
}
foreach($mirror in @($script:playroomBlocks|Where-Object{$_.Type-eq'Mirror'-and$null-ne$_.MirrorShine})){
$mirror.MirrorShine.Opacity=.18+(.14*[Math]::Sin($script:inputTick*.035))
}
}
function Copy-PlayroomBlock($sourceBlock) {
if ($null -eq $sourceBlock -or -not $script:playroomActive) { return }
$beforeCount=$script:playroomBlocks.Count
Add-PlayroomBlock $sourceBlock.Type
if ($script:playroomBlocks.Count -le $beforeCount) { return }
$copy=$script:playroomBlocks[$script:playroomBlocks.Count-1]
$screenLeft=[Windows.SystemParameters]::VirtualScreenLeft
$screenTop=[Windows.SystemParameters]::VirtualScreenTop
$screenRight=$screenLeft+[Windows.SystemParameters]::VirtualScreenWidth
$screenBottom=$screenTop+[Windows.SystemParameters]::VirtualScreenHeight
$copy.Width=$sourceBlock.Width;$copy.Height=$sourceBlock.Height
$copy.X=[Math]::Min($screenRight-$copy.Width,[Math]::Max($screenLeft,$sourceBlock.X+24))
$copy.Y=[Math]::Min($screenBottom-$copy.Height,[Math]::Max($screenTop,$sourceBlock.Y+24))
$copy.Window.Width=$copy.Width;$copy.Window.Height=$copy.Height
$copy.Window.Left=$copy.X;$copy.Window.Top=$copy.Y
Set-BlockTint $copy $sourceBlock.TintIndex
Set-PlacementAngle $copy $sourceBlock.Angle
if($sourceBlock.Type-eq'Conveyor'){Set-ConveyorDirection $copy $sourceBlock.Direction}
}
function Close-FreeRotationHandle {
if($null-ne$script:activeRotationHandleWindow){try{$script:activeRotationHandleWindow.Close()}catch{}}
$script:activeRotationHandleWindow=$null;$script:activeRotationData=$null
}
function Update-FreeRotationHandlePosition {
if($null-eq$script:activeRotationHandleWindow-or$null-eq$script:activeRotationData){return}
$data=$script:activeRotationData
if($null-eq$data.Window-or-not$data.Window.IsVisible){Close-FreeRotationHandle;return}
$centreX=$data.X+($data.Width/2)
$script:activeRotationHandleWindow.Left=$centreX-19
$script:activeRotationHandleWindow.Top=$data.Window.Top-42
}
function Update-FreeRotationFromCursor($rotationState) {
if($null-eq$rotationState-or$null-eq$rotationState.Data){return}
$cursor=New-Object PetNative+POINT
if(-not[PetNative]::GetCursorPos([ref]$cursor)){return}
$centreX=$rotationState.Data.X+($rotationState.Data.Width/2)
$centreY=$rotationState.Data.Y+($rotationState.Data.Height/2)
$pointerAngle=[Math]::Atan2($cursor.Y-$centreY,$cursor.X-$centreX)*180/[Math]::PI
Set-PlacementAngle $rotationState.Data ($pointerAngle-$rotationState.Offset)
}
function Show-FreeRotationHandle($data) {
if($null-eq$data){return}
if($script:activeRotationData-eq$data-and$null-ne$script:activeRotationHandleWindow){Close-FreeRotationHandle;return}
Close-FreeRotationHandle
$handleWindow=New-Object Windows.Window
$handleWindow.Title='Windowisp Free Rotate';$handleWindow.Width=38;$handleWindow.Height=38
$handleWindow.WindowStyle=[Windows.WindowStyle]::None;$handleWindow.AllowsTransparency=$true
$handleWindow.Background=[Windows.Media.Brushes]::Transparent;$handleWindow.ShowInTaskbar=$false
$handleWindow.ShowActivated=$false;$handleWindow.Topmost=$true;$handleWindow.ResizeMode=[Windows.ResizeMode]::NoResize
$circle=New-Object Windows.Controls.Border
$circle.Width=34;$circle.Height=34;$circle.CornerRadius=[Windows.CornerRadius]::new(17)
$circle.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(238,38,34,56))
$circle.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(121,211,255))
$circle.BorderThickness=[Windows.Thickness]::new(2);$circle.Cursor=[Windows.Input.Cursors]::Hand
$arrow=New-Object Windows.Controls.TextBlock
$arrow.Text=[string][char]0x21BB;$arrow.FontFamily=New-Object Windows.Media.FontFamily('Segoe UI Symbol')
$arrow.FontSize=24;$arrow.FontWeight=[Windows.FontWeights]::Bold
$arrow.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(164,229,255))
$arrow.HorizontalAlignment='Center';$arrow.VerticalAlignment='Center';$arrow.Margin=[Windows.Thickness]::new(0,-3,0,0)
$circle.Child=$arrow
$state=[pscustomobject]@{Data=$data;Dragging=$false;Offset=0.0}
$circle.Tag=$state
$circle.Add_MouseLeftButtonDown({
param($sender,$eventArgs)
$cursor=New-Object PetNative+POINT
if([PetNative]::GetCursorPos([ref]$cursor)){
$centreX=$sender.Tag.Data.X+($sender.Tag.Data.Width/2);$centreY=$sender.Tag.Data.Y+($sender.Tag.Data.Height/2)
$pointerAngle=[Math]::Atan2($cursor.Y-$centreY,$cursor.X-$centreX)*180/[Math]::PI
$sender.Tag.Offset=$pointerAngle-$sender.Tag.Data.Angle
}
$sender.Tag.Dragging=$true;$sender.CaptureMouse()|Out-Null;$eventArgs.Handled=$true
})
$circle.Add_MouseMove({param($sender,$eventArgs);if($sender.Tag.Dragging-and$eventArgs.LeftButton-eq[Windows.Input.MouseButtonState]::Pressed){Update-FreeRotationFromCursor $sender.Tag}})
$circle.Add_MouseLeftButtonUp({param($sender,$eventArgs);if($sender.Tag.Dragging){Update-FreeRotationFromCursor $sender.Tag};$sender.Tag.Dragging=$false;$sender.ReleaseMouseCapture();$eventArgs.Handled=$true})
$handleWindow.Content=$circle
$script:activeRotationHandleWindow=$handleWindow;$script:activeRotationData=$data
Update-FreeRotationHandlePosition
$handleWindow.Show()
}
function Set-PlacementAngle($data,[double]$angle) {
if($null-eq$data){return}
$data.Angle=(($angle%360)+360)%360
if($data.Kind-eq'Laser'){
if($null-eq$data.PSObject.Properties['EmitterRotation']){
$data|Add-Member -NotePropertyName EmitterRotation -NotePropertyValue ([Windows.Media.RotateTransform]::new($data.Angle))
$data.Emitter.RenderTransformOrigin=[Windows.Point]::new(.5,.5)
$data.Emitter.RenderTransform=$data.EmitterRotation
}else{$data.EmitterRotation.Angle=$data.Angle}
Update-FreeRotationHandlePosition
return
}
if($data.Kind-eq'Fan'){
$data.Label.Text="FAN $([Math]::Round($data.Angle)) deg"
}
if($null-eq$data.PSObject.Properties['RotationRoot']){
$root=$data.Window.Content
$data.Window.Content=$null
$rotationCanvas=New-Object Windows.Controls.Canvas
$rotationCanvas.ClipToBounds=$false
$rotationCanvas.Children.Add($root)|Out-Null
$data|Add-Member -NotePropertyName RotationRoot -NotePropertyValue $root
$data|Add-Member -NotePropertyName RotationHost -NotePropertyValue $rotationCanvas
$data.Window.Content=$rotationCanvas
}
if($null-eq$data.PSObject.Properties['RotationTransform']){
$data|Add-Member -NotePropertyName RotationTransform -NotePropertyValue (New-Object Windows.Media.RotateTransform $data.Angle)
$data.RotationRoot.RenderTransformOrigin=[Windows.Point]::new(.5,.5)
$data.RotationRoot.RenderTransform=$data.RotationTransform
}else{$data.RotationTransform.Angle=$data.Angle}
$continuous=$null-ne$data.PSObject.Properties['ContinuousRotation']-and$data.ContinuousRotation
$radians=$data.Angle*[Math]::PI/180
if($continuous){
$diagonal=[Math]::Ceiling([Math]::Sqrt(($data.Width*$data.Width)+($data.Height*$data.Height)))+4
$boundWidth=$diagonal;$boundHeight=$diagonal
}else{
$cos=[Math]::Abs([Math]::Cos($radians));$sin=[Math]::Abs([Math]::Sin($radians))
$boundWidth=[Math]::Max(1,($data.Width*$cos)+($data.Height*$sin))
$boundHeight=[Math]::Max(1,($data.Width*$sin)+($data.Height*$cos))
}
if($continuous-and-not[double]::IsNaN($data.RotationHost.Width)-and[Math]::Abs($data.Window.Width-$boundWidth)-le.25-and[Math]::Abs($data.Window.Height-$boundHeight)-le.25){Update-FreeRotationHandlePosition;return}
$centreX=$data.X+($data.Width/2);$centreY=$data.Y+($data.Height/2)
$data.RotationRoot.Width=$data.Width;$data.RotationRoot.Height=$data.Height
[Windows.Controls.Canvas]::SetLeft($data.RotationRoot,($boundWidth-$data.Width)/2)
[Windows.Controls.Canvas]::SetTop($data.RotationRoot,($boundHeight-$data.Height)/2)
if([double]::IsNaN($data.RotationHost.Width)-or[double]::IsNaN($data.RotationHost.Height)-or[Math]::Abs($data.Window.Width-$boundWidth)-gt.25-or[Math]::Abs($data.Window.Height-$boundHeight)-gt.25){
$data.RotationHost.Width=$boundWidth;$data.RotationHost.Height=$boundHeight
$data.Window.Width=$boundWidth;$data.Window.Height=$boundHeight
$data.Window.Left=$centreX-($boundWidth/2);$data.Window.Top=$centreY-($boundHeight/2)
}
Update-FreeRotationHandlePosition
}
function Resolve-PlayerRotatedBlock($block,[double]$playerX,[double]$playerY,[double]$velocityX,[double]$velocityY) {
if([Math]::Abs($block.Angle)-lt.01){return $null}
$r=$block.Angle*[Math]::PI/180
$ux=[Math]::Cos($r);$uy=[Math]::Sin($r);$vx=-[Math]::Sin($r);$vy=[Math]::Cos($r)
$blockCenterX=$block.X+$block.Width/2;$blockCenterY=$block.Y+$block.Height/2
$playerCenterX=$playerX+$PetWidth/2;$playerCenterY=$playerY+$PetHeight/2
$playerHalfWidth=$PetWidth/2-12;$playerHalfHeight=$PetHeight/2-5
$deltaX=$playerCenterX-$blockCenterX;$deltaY=$playerCenterY-$blockCenterY
$minimumOverlap=[double]::PositiveInfinity;$normalX=0.0;$normalY=0.0
foreach($axis in @(@(1.0,0.0),@(0.0,1.0),@($ux,$uy),@($vx,$vy))){
$axisX=[double]$axis[0];$axisY=[double]$axis[1]
$distance=($deltaX*$axisX)+($deltaY*$axisY)
$playerRadius=($playerHalfWidth*[Math]::Abs($axisX))+($playerHalfHeight*[Math]::Abs($axisY))
$blockRadius=($block.Width/2*[Math]::Abs(($ux*$axisX)+($uy*$axisY)))+($block.Height/2*[Math]::Abs(($vx*$axisX)+($vy*$axisY)))
$overlap=$playerRadius+$blockRadius-[Math]::Abs($distance)
if($overlap-le0){return $null}
if($overlap-lt$minimumOverlap){
$minimumOverlap=$overlap
$sign=$(if($distance-ge0){1.0}else{-1.0})
$normalX=$axisX*$sign;$normalY=$axisY*$sign
}
}
$playerCenterX+=$normalX*($minimumOverlap+.25);$playerCenterY+=$normalY*($minimumOverlap+.25)
$inwardVelocity=($velocityX*$normalX)+($velocityY*$normalY)
if($inwardVelocity-lt0){$velocityX-=$inwardVelocity*$normalX;$velocityY-=$inwardVelocity*$normalY}
$topNormalX=$vx*-1;$topNormalY=$vy*-1
$topFacing=(($normalX*$topNormalX)+($normalY*$topNormalY))-gt.62
return [pscustomobject]@{
X=$playerCenterX-$PetWidth/2;Y=$playerCenterY-$PetHeight/2;VX=$velocityX;VY=$velocityY
Grounded=($topFacing-and$normalY-lt-.25);Wall=if([Math]::Abs($normalX)-gt.55){[Math]::Sign($normalX)*-1}else{0}
Block=$block
}
}
function Resolve-PlayerWoodenPlatform($block,[double]$oldX,[double]$oldY,[double]$playerX,[double]$playerY,[double]$velocityX,[double]$velocityY,[int]$dropTicks) {
# Clouds share the one-way top-face behaviour of wooden platforms, while
# retaining their own temporary puff/fade lifecycle after a landing.
if($block.Type-notin@('Wooden','Cloud')-or$block.TemporaryState-eq'Gone'-or$dropTicks-gt0){return $null}
$r=$block.Angle*[Math]::PI/180
$alongX=[Math]::Cos($r);$alongY=[Math]::Sin($r)
$topX=[Math]::Sin($r);$topY=-[Math]::Cos($r)
$topCentreX=$block.X+($block.Width/2)+($topX*$block.Height/2)
$topCentreY=$block.Y+($block.Height/2)+($topY*$block.Height/2)
$oldFootX=$oldX+($PetWidth/2);$oldFootY=$oldY+$PetHeight
$newFootX=$playerX+($PetWidth/2);$newFootY=$playerY+$PetHeight
$oldDistance=(($oldFootX-$topCentreX)*$topX)+(($oldFootY-$topCentreY)*$topY)
$newDistance=(($newFootX-$topCentreX)*$topX)+(($newFootY-$topCentreY)*$topY)
$alongDistance=(($newFootX-$topCentreX)*$alongX)+(($newFootY-$topCentreY)*$alongY)
$approach=($velocityX*$topX)+($velocityY*$topY)
if($approach-ge.1-or$oldDistance-lt-6-or$newDistance-gt0-or[Math]::Abs($alongDistance)-gt($block.Width/2+($PetWidth*.25))){return $null}
$correction=-$newDistance
$playerX+=$topX*$correction;$playerY+=$topY*$correction
if($approach-lt0){$velocityX-=$approach*$topX;$velocityY-=$approach*$topY}
return [pscustomobject]@{X=$playerX;Y=$playerY;VX=$velocityX;VY=$velocityY;Grounded=($topY-lt-.25);Block=$block}
}
function Resolve-BallRotatedBlock($ball,$block,[double]$restitution) {
if([Math]::Abs($block.Angle)-lt.01){return $false}
$r=$block.Angle*[Math]::PI/180
$ux=[Math]::Cos($r);$uy=[Math]::Sin($r);$vx=-[Math]::Sin($r);$vy=[Math]::Cos($r)
$blockCenterX=$block.X+$block.Width/2;$blockCenterY=$block.Y+$block.Height/2
$ballCenterX=$ball.X+$ball.Radius;$ballCenterY=$ball.Y+$ball.Radius
$dx=$ballCenterX-$blockCenterX;$dy=$ballCenterY-$blockCenterY
$localX=($dx*$ux)+($dy*$uy);$localY=($dx*$vx)+($dy*$vy)
$halfWidth=$block.Width/2;$halfHeight=$block.Height/2
$closestX=[Math]::Max(-$halfWidth,[Math]::Min($halfWidth,$localX))
$closestY=[Math]::Max(-$halfHeight,[Math]::Min($halfHeight,$localY))
$normalLocalX=$localX-$closestX;$normalLocalY=$localY-$closestY
$distanceSquared=($normalLocalX*$normalLocalX)+($normalLocalY*$normalLocalY)
if($distanceSquared-ge($ball.Radius*$ball.Radius)){return $false}
if($distanceSquared-lt.0001){
$edgeX=$halfWidth-[Math]::Abs($localX);$edgeY=$halfHeight-[Math]::Abs($localY)
if($edgeX-lt$edgeY){$normalLocalX=$(if($localX-ge0){1.0}else{-1.0});$normalLocalY=0;$penetration=$ball.Radius+$edgeX}
else{$normalLocalX=0;$normalLocalY=$(if($localY-ge0){1.0}else{-1.0});$penetration=$ball.Radius+$edgeY}
}else{
$distance=[Math]::Sqrt($distanceSquared);$normalLocalX/=$distance;$normalLocalY/=$distance;$penetration=$ball.Radius-$distance
}
$normalX=($normalLocalX*$ux)+($normalLocalY*$vx);$normalY=($normalLocalX*$uy)+($normalLocalY*$vy)
$ballCenterX+=$normalX*($penetration+.2);$ballCenterY+=$normalY*($penetration+.2)
$ball.X=$ballCenterX-$ball.Radius;$ball.Y=$ballCenterY-$ball.Radius
$inward=($ball.VX*$normalX)+($ball.VY*$normalY)
if($inward-lt0){$ball.VX-=(1+$restitution)*$inward*$normalX;$ball.VY-=(1+$restitution)*$inward*$normalY}
$topNormalX=$vx*-1;$topNormalY=$vy*-1
$topFacing=(($normalX*$topNormalX)+($normalY*$topNormalY))-gt.62
if($topFacing-and$block.Type-eq'Bouncy'){$ball.VX+=$topNormalX*8;$ball.VY+=$topNormalY*8}
elseif($topFacing-and$block.Type-eq'Fire'){$ball.VX+=$topNormalX*5;$ball.VY+=$topNormalY*5}
elseif($topFacing-and$block.Type-eq'Ice'){$ball.VX*=1.06;$ball.VY*=1.06}
elseif($topFacing-and$block.Type-eq'Conveyor'){
$ball.VX=[Math]::Max(-16,[Math]::Min(16,$ball.VX+($ux*$block.Direction*.85)))
$ball.VY=[Math]::Max(-16,[Math]::Min(16,$ball.VY+($uy*$block.Direction*.85)))
}
return $true
}
function Resolve-DynamicPlacementRotatedBlock($placement,$block,[double]$restitution) {
# Hazards have transparent artwork around a smaller physical body. Reuse the
# proven swept-circle rotated-block solver against that real body, then map
# the corrected centre back to the placement window.
$bodyW=[double]$placement.BodyWidth;$bodyH=[double]$placement.BodyHeight
$offX=[double]$placement.BodyOffsetX;$offY=[double]$placement.BodyOffsetY
$radius=[Math]::Max(5.0,[Math]::Min($bodyW,$bodyH)*.5)
$proxy=[pscustomobject]@{
X=[double]($placement.X+$offX+($bodyW/2)-$radius)
Y=[double]($placement.Y+$offY+($bodyH/2)-$radius)
VX=[double]$placement.VX;VY=[double]$placement.VY;Radius=[double]$radius
}
if(-not(Resolve-BallRotatedBlock $proxy $block $restitution)){return $false}
$placement.X=$proxy.X+$radius-($bodyW/2)-$offX
$placement.Y=$proxy.Y+$radius-($bodyH/2)-$offY
$placement.VX=$proxy.VX;$placement.VY=$proxy.VY
return $true
}
function New-BlockMenuIcon([string]$kind, $colour = $null) {
$canvas=New-Object Windows.Controls.Canvas
$canvas.Width=16;$canvas.Height=16
$stroke=$(if($null-ne$colour){$colour}else{New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(225,220,239))})
if($kind-eq'Copy'){
$back=New-Object Windows.Shapes.Rectangle;$back.Width=8;$back.Height=8;$back.RadiusX=1;$back.RadiusY=1
$back.Stroke=$stroke;$back.StrokeThickness=1.5
[Windows.Controls.Canvas]::SetLeft($back,2);[Windows.Controls.Canvas]::SetTop($back,2);$canvas.Children.Add($back)|Out-Null
$front=New-Object Windows.Shapes.Rectangle;$front.Width=8;$front.Height=8;$front.RadiusX=1;$front.RadiusY=1
$front.Fill=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(31,28,45))
$front.Stroke=$stroke;$front.StrokeThickness=1.5
[Windows.Controls.Canvas]::SetLeft($front,6);[Windows.Controls.Canvas]::SetTop($front,6);$canvas.Children.Add($front)|Out-Null
}elseif($kind-eq'Colour'){
foreach($dot in @(@(3,3,([Windows.Media.Color]::FromRgb(91,143,249))),@(9,3,([Windows.Media.Color]::FromRgb(236,79,132))),@(6,9,([Windows.Media.Color]::FromRgb(242,186,73))))){
$circle=New-Object Windows.Shapes.Ellipse;$circle.Width=5;$circle.Height=5
$circle.Fill=New-Object Windows.Media.SolidColorBrush $dot[2]
[Windows.Controls.Canvas]::SetLeft($circle,$dot[0]);[Windows.Controls.Canvas]::SetTop($circle,$dot[1]);$canvas.Children.Add($circle)|Out-Null
}
}elseif($kind-eq'Delete'){
foreach($coords in @(@(4,4,12,12),@(12,4,4,12))){
$line=New-Object Windows.Shapes.Line
$line.X1=$coords[0];$line.Y1=$coords[1];$line.X2=$coords[2];$line.Y2=$coords[3]
$line.Stroke=$stroke;$line.StrokeThickness=2;$line.StrokeStartLineCap='Round';$line.StrokeEndLineCap='Round'
$canvas.Children.Add($line)|Out-Null
}
}elseif($kind-eq'Swatch'){
$swatch=New-Object Windows.Shapes.Ellipse;$swatch.Width=10;$swatch.Height=10
$swatch.Fill=$stroke;$swatch.Stroke=[Windows.Media.Brushes]::White;$swatch.StrokeThickness=1
[Windows.Controls.Canvas]::SetLeft($swatch,3);[Windows.Controls.Canvas]::SetTop($swatch,3);$canvas.Children.Add($swatch)|Out-Null
}elseif($kind-eq'Rope'){
$ropeLine=New-Object Windows.Shapes.Path
$ropeLine.Stroke=$stroke;$ropeLine.StrokeThickness=2
$ropeLine.Data=[Windows.Media.Geometry]::Parse('M 2,4 C 5,14 11,2 14,12')
$canvas.Children.Add($ropeLine)|Out-Null
}elseif($kind-eq'Rotate'){
$rotatePath=New-Object Windows.Shapes.Path
$rotatePath.Stroke=$stroke;$rotatePath.StrokeThickness=1.8
$rotatePath.StrokeStartLineCap='Round';$rotatePath.StrokeEndLineCap='Round'
$rotatePath.Data=[Windows.Media.Geometry]::Parse('M 4,6 A 5,5 0 1 1 4,11 M 4,6 L 4,2 M 4,6 L 8,6')
$canvas.Children.Add($rotatePath)|Out-Null
}
$iconTile=New-Object Windows.Controls.Border
# Deliberately overlap the system icon-column boundary so the native
# white partition can never show through in idle or hover states.
$iconTile.Width=38;$iconTile.Height=26
$iconTile.Margin=[Windows.Thickness]::new(-6,-4,-16,-4)
$iconTile.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(20,43,67))
$canvas.HorizontalAlignment=[Windows.HorizontalAlignment]::Left
$canvas.Margin=[Windows.Thickness]::new(6,0,0,0)
$iconTile.Child=$canvas
return $iconTile
}
function Set-WindowispContextMenuStyle($menu) {
$navy=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(20,43,67))
$menu.Resources[[Windows.SystemColors]::MenuBrushKey]=$navy
$menu.Resources[[Windows.SystemColors]::ControlBrushKey]=$navy
$menu.Resources[[Windows.SystemColors]::HighlightBrushKey]=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(43,91,128))
$menu.Resources[[Windows.SystemColors]::HighlightTextBrushKey]=[Windows.Media.Brushes]::White
$menu.Background=$navy;$menu.Foreground=[Windows.Media.Brushes]::White
$menu.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(104,168,211))
$menu.BorderThickness=[Windows.Thickness]::new(1)
return $navy
}
function Set-WindowispSubmenuStyle($item,$navy) {
$item.Background=$navy;$item.Foreground=[Windows.Media.Brushes]::White
$item.Resources[[Windows.SystemColors]::MenuBrushKey]=$navy
$item.Resources[[Windows.SystemColors]::ControlBrushKey]=$navy
$item.Resources[[Windows.SystemColors]::HighlightBrushKey]=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(43,91,128))
$item.Resources[[Windows.SystemColors]::HighlightTextBrushKey]=[Windows.Media.Brushes]::White
}
function New-BlockContextMenu($blockData) {
$menu=New-Object Windows.Controls.ContextMenu
$iconRailBrush=Set-WindowispContextMenuStyle $menu
$copy=New-Object Windows.Controls.MenuItem
$copy.Header='Duplicate Block';$copy.Tag=$blockData;$copy.Icon=New-BlockMenuIcon 'Copy'
$copy.Add_Click({param($sender,$eventArgs) Copy-PlayroomBlock $sender.Tag})
$menu.Items.Add($copy)|Out-Null
$recolour=New-Object Windows.Controls.MenuItem
$recolour.Header='Block Colour';$recolour.Icon=New-BlockMenuIcon 'Colour'
Set-WindowispSubmenuStyle $recolour $iconRailBrush
$tintNames=@('Default','Violet','Ocean','Forest','Gold','Rose')
$tintColours=@(
[Windows.Media.Color]::FromRgb(172,165,188),
[Windows.Media.Color]::FromRgb(145,91,219),
[Windows.Media.Color]::FromRgb(52,143,218),
[Windows.Media.Color]::FromRgb(54,166,103),
[Windows.Media.Color]::FromRgb(224,169,55),
[Windows.Media.Color]::FromRgb(226,91,132)
)
for($i=0;$i-lt$tintNames.Count;$i++){
$tintItem=New-Object Windows.Controls.MenuItem
$tintItem.Header=$tintNames[$i]
$tintItem.IsCheckable=$true;$tintItem.IsChecked=([int]$blockData.TintIndex-eq$i)
Set-WindowispSubmenuStyle $tintItem $iconRailBrush
$tintItem.Icon=New-BlockMenuIcon 'Swatch' (New-Object Windows.Media.SolidColorBrush $tintColours[$i])
$tintItem.Tag=[pscustomobject]@{Data=$blockData;Index=$i;Menu=$recolour}
$tintItem.Add_Click({param($sender,$eventArgs) Set-BlockTint $sender.Tag.Data $sender.Tag.Index;foreach($item in @($sender.Tag.Menu.Items)){if($item.IsCheckable){$item.IsChecked=($item-eq$sender)}}})
$recolour.Items.Add($tintItem)|Out-Null
}
$menu.Items.Add($recolour)|Out-Null
if($blockData.Type-eq'Conveyor'){
$reverse=New-Object Windows.Controls.MenuItem
$reverse.Header='Reverse Direction';$reverse.Icon=New-BlockMenuIcon 'Rotate';$reverse.Tag=$blockData
$reverse.Add_Click({param($sender,$eventArgs)Set-ConveyorDirection $sender.Tag (-1*$sender.Tag.Direction)})
$menu.Items.Add($reverse)|Out-Null
}
$rotate=New-Object Windows.Controls.MenuItem
$rotate.Header='Rotate Freely';$rotate.Icon=New-BlockMenuIcon 'Rotate';$rotate.Tag=$blockData
$rotate.Add_Click({param($sender,$eventArgs)Show-FreeRotationHandle $sender.Tag})
$menu.Items.Add($rotate)|Out-Null
$resetRotate=New-Object Windows.Controls.MenuItem
$resetRotate.Header='Reset Angle';$resetRotate.Icon=New-BlockMenuIcon 'Rotate';$resetRotate.Tag=$blockData
$resetRotate.Add_Click({param($sender,$eventArgs)Set-PlacementAngle $sender.Tag 0})
$menu.Items.Add($resetRotate)|Out-Null
$delete=New-Object Windows.Controls.MenuItem
$delete.Header='Delete Block';$delete.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,116,126))
$delete.Icon=New-BlockMenuIcon 'Delete' $delete.Foreground
$delete.Tag=$blockData
$delete.Add_Click({param($sender,$eventArgs) Remove-PlayroomBlock $sender.Tag})
$menu.Items.Add($delete)|Out-Null
return $menu
}
function Remove-PlayroomPlacement($data) {
if($null-eq$data){return}
if($script:activeRotationData-eq$data){Close-FreeRotationHandle}
if($data.Kind-ne'Rope'){Remove-RopesForPlacement $data}
foreach($wire in @($script:playroomWires|Where-Object{$_.Source-eq$data-or$_.Target-eq$data})){Remove-PlayroomPlacement $wire}
foreach($rotator in @($script:playroomGadgets|Where-Object{$_.Kind-eq'Rotator'-and$_.Target-eq$data})){
$rotator.Target=$null
if($null-ne$data.PSObject.Properties['ContinuousRotation']){$data.ContinuousRotation=$false;Set-PlacementAngle $data $data.Angle}
}
if($data.Kind-eq'Wire'){$script:playroomWires.Remove($data)}
elseif($data.Kind-eq'Block'){$script:playroomBlocks.Remove($data)}
elseif($script:playroomBalls.Contains($data)){$script:playroomBalls.Remove($data)}
else{
if($data.Kind-eq'Rotator'-and$null-ne$data.Target){
if($null-ne$data.Target.PSObject.Properties['ContinuousRotation']){$data.Target.ContinuousRotation=$false;Set-PlacementAngle $data.Target $data.Target.Angle}
}
$script:playroomGadgets.Remove($data)
}
try{$data.Window.Close()}catch{}
if($null-ne$data.PSObject.Properties['BeamWindow']-and$null-ne$data.BeamWindow){try{$data.BeamWindow.Close()}catch{}}
if($data.Kind-eq'Rope'-and$null-ne$script:ropeOverlayCanvas){
# Always release physics ownership from both endpoints. This also repairs
# an older/stale rope state where FrozenOther was no longer the endpoint
# carrying the marker, which otherwise left that object floating forever.
foreach($endpoint in @($data.EndA,$data.EndB,$data.FrozenOther)){
if($null-ne$endpoint-and$null-ne$endpoint.PSObject.Properties['FrozenRope']-and$endpoint.FrozenRope-eq$data){$endpoint.FrozenRope=$null}
}
foreach($ropePath in @($data.VisualPaths)){$script:ropeOverlayCanvas.Children.Remove($ropePath)|Out-Null}
}
if($data.Kind-eq'Wire'-and$null-ne$script:ropeOverlayCanvas){foreach($wirePath in @($data.VisualPaths)){$script:ropeOverlayCanvas.Children.Remove($wirePath)|Out-Null}}
if($null-ne$data.PSObject.Properties['WindLines']-and$null-ne$script:ropeOverlayCanvas){foreach($wind in @($data.WindLines)){$script:ropeOverlayCanvas.Children.Remove($wind.Line)|Out-Null}}
}
function Get-EraserPlacements {
return @($script:playroomBlocks)+@($script:playroomBalls)+@($script:playroomGadgets)+@($script:playroomWires)
}
function Update-EraserButtonVisual {
if($null-eq$script:eraserButton){return}
if($script:eraserMode){
$script:eraserButton.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(150,43,66))
$script:eraserButton.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,132,151))
$script:eraserButton.ToolTip='Eraser active — click items to delete, Esc to exit'
}else{
$script:eraserButton.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(27,57,82))
$script:eraserButton.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(92,79,123))
$script:eraserButton.ToolTip='Eraser Mode'
}
}
function Set-EraserMode([bool]$enabled) {
if($enabled){Set-RopeToolMode $false;Set-WiringMode $false}
$script:eraserMode=$enabled-and$script:playroomActive
if($script:eraserMode){
Close-FreeRotationHandle
$script:ropeConnectSource=$null;$script:ropeConnectFirst=$null
if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='ERASER ACTIVE | CLICK ITEMS TO DELETE | ESC TO EXIT'}
}
foreach($data in @(Get-EraserPlacements)){
if($null-eq$data-or$null-eq$data.Window){continue}
$data.Window.Cursor=$(if($script:eraserMode){[Windows.Input.Cursors]::Cross}else{$null})
if($null-ne$data.PSObject.Properties['Element']-and$null-ne$data.Element){$data.Element.Opacity=1}
}
Update-EraserButtonVisual
}
function Toggle-EraserMode { Set-EraserMode (-not$script:eraserMode) }
function Update-GridButtonVisual {
if($null-eq$script:gridButton){return}
$script:gridButton.Background=New-Object Windows.Media.SolidColorBrush $(if($script:gridSnapEnabled){[Windows.Media.Color]::FromRgb(43,139,176)}else{[Windows.Media.Color]::FromRgb(27,57,82)})
$script:gridButton.BorderBrush=New-Object Windows.Media.SolidColorBrush $(if($script:gridSnapEnabled){[Windows.Media.Color]::FromRgb(107,225,244)}else{[Windows.Media.Color]::FromRgb(92,79,123)})
$script:gridButton.ToolTip=$(if($script:gridSnapEnabled){'Grid snapping active'}else{'Toggle grid snapping'})
}
function New-GridOverlay {
if($null-ne$script:gridOverlayWindow){return}
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$width=[Windows.SystemParameters]::VirtualScreenWidth;$height=[Windows.SystemParameters]::VirtualScreenHeight
$script:gridOverlayWindow=New-Object Windows.Window
$script:gridOverlayWindow.Title='Windowisp Builder Grid'
$script:gridOverlayWindow.Left=$left;$script:gridOverlayWindow.Top=$top
$script:gridOverlayWindow.Width=$width;$script:gridOverlayWindow.Height=$height
$script:gridOverlayWindow.WindowStyle=[Windows.WindowStyle]::None
$script:gridOverlayWindow.AllowsTransparency=$true;$script:gridOverlayWindow.Background=[Windows.Media.Brushes]::Transparent
$script:gridOverlayWindow.ShowInTaskbar=$false;$script:gridOverlayWindow.ShowActivated=$false
$script:gridOverlayWindow.ResizeMode=[Windows.ResizeMode]::NoResize;$script:gridOverlayWindow.Topmost=$true
$canvas=New-Object Windows.Controls.Canvas
$brush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(62,91,205,230))
for($x=0.0;$x-le$width;$x+=$script:gridSize){$line=New-Object Windows.Shapes.Line;$line.X1=$x;$line.X2=$x;$line.Y1=0;$line.Y2=$height;$line.Stroke=$brush;$line.StrokeThickness=1;$canvas.Children.Add($line)|Out-Null}
for($y=0.0;$y-le$height;$y+=$script:gridSize){$line=New-Object Windows.Shapes.Line;$line.X1=0;$line.X2=$width;$line.Y1=$y;$line.Y2=$y;$line.Stroke=$brush;$line.StrokeThickness=1;$canvas.Children.Add($line)|Out-Null}
$script:gridOverlayWindow.Content=$canvas
}
function Set-GridSnap([bool]$enabled) {
$script:gridSnapEnabled=$enabled-and$script:playroomActive
if($script:gridSnapEnabled){
New-GridOverlay
if($null-ne$script:gridOverlayWindow-and-not$script:gridOverlayWindow.IsVisible){$script:gridOverlayWindow.Show();Set-OverlayClickThroughState $script:gridOverlayWindow $true}
}elseif($null-ne$script:gridOverlayWindow){$script:gridOverlayWindow.Hide()}
Update-GridButtonVisual
}
function Toggle-GridSnap { Set-GridSnap (-not$script:gridSnapEnabled) }
function Snap-PlacementToGrid($data,[bool]$snapSize=$false) {
if(-not$script:gridSnapEnabled-or$null-eq$data){return}
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$data.X=$left+([Math]::Round(($data.X-$left)/$script:gridSize)*$script:gridSize)
$data.Y=$top+([Math]::Round(($data.Y-$top)/$script:gridSize)*$script:gridSize)
if($snapSize-and$null-ne$data.PSObject.Properties['Width']){
$minimumHeight=$(if($data.Type-eq'Wooden'){8.0}else{20.0})
$data.Width=[Math]::Max(60.0,[Math]::Round($data.Width/$script:gridSize)*$script:gridSize)
$data.Height=[Math]::Max($minimumHeight,[Math]::Round($data.Height/$script:gridSize)*$script:gridSize)
}
}
function Update-ConnectionToolVisuals {
if($null-ne$script:ropeToolButton){$script:ropeToolButton.Background=New-Object Windows.Media.SolidColorBrush $(if($script:ropeToolMode){[Windows.Media.Color]::FromRgb(151,103,46)}else{[Windows.Media.Color]::FromRgb(27,57,82)})}
if($null-ne$script:wiringButton){$script:wiringButton.Background=New-Object Windows.Media.SolidColorBrush $(if($script:wiringMode){[Windows.Media.Color]::FromRgb(43,139,176)}else{[Windows.Media.Color]::FromRgb(27,57,82)})}
}
function Update-PlacementToolCursors {
foreach($data in @(Get-EraserPlacements)){
if($null-ne$data-and$null-ne$data.Window){$data.Window.Cursor=$(if($script:eraserMode-or$script:ropeToolMode-or$script:wiringMode){[Windows.Input.Cursors]::Cross}else{$null})}
}
}
function Set-RopeToolMode([bool]$enabled) {
$script:ropeToolMode=$enabled-and$script:playroomActive
if($script:ropeToolMode){$script:wiringMode=$false;$script:eraserMode=$false;$script:ropeConnectSource=$null;$script:ropeConnectFirst=$null;if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='ROPE TOOL: CLICK FIRST ITEM'}}
elseif($null-ne$script:sandboxTrayTitle-and$script:sandboxTrayTitle.Text-like'ROPE TOOL*'){$script:sandboxTrayTitle.Text='GADGET LAB'}
Update-ConnectionToolVisuals;Update-EraserButtonVisual;Update-PlacementToolCursors
}
function Set-WiringMode([bool]$enabled) {
$script:wiringMode=$enabled-and$script:playroomActive
if($script:wiringMode){$script:ropeToolMode=$false;$script:eraserMode=$false;$script:wireConnectFirst=$null;if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='WIRING: CLICK INPUT OR HAZARD'}}
elseif($null-ne$script:sandboxTrayTitle-and$script:sandboxTrayTitle.Text-like'WIRING*'){$script:sandboxTrayTitle.Text='GADGET LAB'}
Update-ConnectionToolVisuals;Update-EraserButtonVisual;Update-PlacementToolCursors
}
function Invoke-ExitControl {
if($script:wiringMode){Set-WiringMode $false;$script:eraserExitGuardTicks=20;return}
if($script:ropeToolMode){Set-RopeToolMode $false;$script:eraserExitGuardTicks=20;return}
if($script:eraserMode){Set-EraserMode $false;$script:eraserExitGuardTicks=20;return}
if($script:eraserExitGuardTicks-gt0){return}
if($script:wispfallActive-or$script:footballActive){Return-ToSandbox;return}
Set-GameMode $false
}
function Copy-PlayroomPlacement($data) {
if($null-eq$data-or-not$script:playroomActive){return}
if($data.Kind-eq'Rope'){New-PlayroomRope $data.EndA $data.EndB $data.Length}
elseif($data.Kind-eq'Block'){Copy-PlayroomBlock $data}
elseif($data.Kind-eq'Ball'){Add-PlayroomBall}
elseif($data.Kind-in@('Bat','Bone')){Add-PlayroomToy $data.Kind}
elseif($data.Kind-in@('Spikes','FireJet','Laser')){Add-PlayroomHazard $data.Kind}
elseif($data.Kind-eq'RopeCoil'){Add-PlayroomGadget 'RopeCoil'}
elseif($data.Kind-like'Teleporter*'){Add-PlayroomGadget 'Teleporter'}
else{Add-PlayroomGadget $data.Kind}
}
function New-PlacementContextMenu($data,[bool]$rotatable=$true) {
$menu=New-Object Windows.Controls.ContextMenu
$navy=Set-WindowispContextMenuStyle $menu
if($data.Kind-ne'Rope'){
$copy=New-Object Windows.Controls.MenuItem;$copy.Header='Duplicate';$copy.Tag=$data;$copy.Icon=New-BlockMenuIcon 'Copy'
$copy.Add_Click({param($sender,$e)Copy-PlayroomPlacement $sender.Tag});$menu.Items.Add($copy)|Out-Null
}
if($data.Kind-eq'RopeCoil'){
$attach=New-Object Windows.Controls.MenuItem;$attach.Header='Connect Rope';$attach.Tag=$data;$attach.Icon=New-BlockMenuIcon 'Rope'
$attach.Add_Click({param($sender,$e)Arm-RopePlacement $sender.Tag});$menu.Items.Add($attach)|Out-Null
}elseif($data.Kind-eq'Rope'){
$unattach=New-Object Windows.Controls.MenuItem;$unattach.Header='Detach Rope';$unattach.Tag=$data;$unattach.Icon=New-BlockMenuIcon 'Rope'
$unattach.Add_Click({param($sender,$e)Unattach-PlayroomRope $sender.Tag});$menu.Items.Add($unattach)|Out-Null
}
if($data.Kind-eq'Ball'){
$currentBounce=[Math]::Max(1,[Math]::Min(100,[Math]::Round($data.Physics)))
$bounceMenu=New-Object Windows.Controls.MenuItem;$bounceMenu.Header="Bounciness: $currentBounce%";Set-WindowispSubmenuStyle $bounceMenu $navy
foreach($bounceOption in @(@('Low (25%)',25.0),@('Normal (50%)',50.0),@('High (75%)',75.0),@('Super (100%)',100.0))){
$bounceItem=New-Object Windows.Controls.MenuItem;$bounceItem.Header=$bounceOption[0];$bounceItem.IsCheckable=$true
Set-WindowispSubmenuStyle $bounceItem $navy
$bounceItem.IsChecked=([Math]::Abs([double]$data.Physics-[double]$bounceOption[1])-lt.1)
$bounceItem.Tag=[pscustomobject]@{Ball=$data;Value=[double]$bounceOption[1];Menu=$bounceMenu}
$bounceItem.Add_Click({
param($sender,$e)
$sender.Tag.Ball.Physics=$sender.Tag.Value
$sender.Tag.Menu.Header="Bounciness: $([Math]::Round($sender.Tag.Value))%"
foreach($item in @($sender.Tag.Menu.Items)){if($item.IsCheckable){$item.IsChecked=($item-eq$sender)}}
})
$bounceMenu.Items.Add($bounceItem)|Out-Null
}
$menu.Items.Add($bounceMenu)|Out-Null
}
if($rotatable){
$rotate=New-Object Windows.Controls.MenuItem;$rotate.Header='Rotate Freely';$rotate.Icon=New-BlockMenuIcon 'Rotate';$rotate.Tag=$data
$rotate.Add_Click({param($sender,$e)Show-FreeRotationHandle $sender.Tag})
$menu.Items.Add($rotate)|Out-Null
$resetRotate=New-Object Windows.Controls.MenuItem;$resetRotate.Header='Reset Angle';$resetRotate.Icon=New-BlockMenuIcon 'Rotate';$resetRotate.Tag=$data
$resetRotate.Add_Click({param($sender,$e)Set-PlacementAngle $sender.Tag 0})
$menu.Items.Add($resetRotate)|Out-Null
}
if($data.Kind-in@('Spikes','FireJet','Laser','Fan','Rotator','TeleporterA','TeleporterB')){
$powerMenu=New-Object Windows.Controls.MenuItem;$powerMenu.Header='Power';Set-WindowispSubmenuStyle $powerMenu $navy
$powerOptions=$(if(Test-WireTarget $data){@(@('Always On',$true),@('Always Off',$false),@('Follow Wiring',$null))}else{@(@('On',$true),@('Off',$false))})
foreach($powerOption in $powerOptions){
$powerItem=New-Object Windows.Controls.MenuItem;$powerItem.Header=$powerOption[0]
Set-WindowispSubmenuStyle $powerItem $navy
$powerItem.IsCheckable=$true
$powerItem.IsChecked=$(if($null-eq$powerOption[1]){$null-eq$data.ManualPower}else{if($null-eq$data.ManualPower){(-not(Test-WireTarget $data))-and[bool]$powerOption[1]}else{[bool]$data.ManualPower-eq[bool]$powerOption[1]}})
$powerItem.Tag=[pscustomobject]@{Placement=$data;ManualPower=$powerOption[1];Menu=$powerMenu}
$powerItem.Add_Click({param($sender,$e)$sender.Tag.Placement.ManualPower=$sender.Tag.ManualPower;foreach($item in @($sender.Tag.Menu.Items)){if($item.IsCheckable){$item.IsChecked=($item-eq$sender)}}})
$powerMenu.Items.Add($powerItem)|Out-Null
}
$menu.Items.Add($powerMenu)|Out-Null
}
if($data.Kind-eq'Rotator'){
$reverse=New-Object Windows.Controls.MenuItem;$reverse.Header=$(if($data.Direction-lt0){'Direction: Counter-clockwise'}else{'Direction: Clockwise'});$reverse.Tag=$data;$reverse.Icon=New-BlockMenuIcon 'Rotate'
$reverse.Add_Click({param($sender,$e)$sender.Tag.Direction*=-1;$sender.Header=$(if($sender.Tag.Direction-lt0){'Direction: Counter-clockwise'}else{'Direction: Clockwise'})})
$menu.Items.Add($reverse)|Out-Null
$currentSpeedName=$(if([double]$data.RotationSpeed-lt.75){'Slow'}elseif([double]$data.RotationSpeed-gt1.5){'Fast'}else{'Normal'})
$speedMenu=New-Object Windows.Controls.MenuItem;$speedMenu.Header="Rotation Speed: $currentSpeedName";$speedMenu.Icon=New-BlockMenuIcon 'Rotate';Set-WindowispSubmenuStyle $speedMenu $navy
foreach($speedOption in @(@('Slow (50%)',0.5,'Slow'),@('Normal (100%)',1.0,'Normal'),@('Fast (200%)',2.0,'Fast'))){
$speedItem=New-Object Windows.Controls.MenuItem;$speedItem.Header=$speedOption[0];$speedItem.IsCheckable=$true
Set-WindowispSubmenuStyle $speedItem $navy
$speedItem.IsChecked=([double]$data.RotationSpeed-eq[double]$speedOption[1])
$speedItem.Tag=[pscustomobject]@{Rotator=$data;Multiplier=[double]$speedOption[1];Name=[string]$speedOption[2];Menu=$speedMenu}
$speedItem.Add_Click({
param($sender,$e)
$sender.Tag.Rotator.RotationSpeed=$sender.Tag.Multiplier
$sender.Tag.Menu.Header="Rotation Speed: $($sender.Tag.Name)"
foreach($item in @($sender.Tag.Menu.Items)){if($item.IsCheckable){$item.IsChecked=($item-eq$sender)}}
})
$speedMenu.Items.Add($speedItem)|Out-Null
}
$menu.Items.Add($speedMenu)|Out-Null
$release=New-Object Windows.Controls.MenuItem;$release.Header='Detach Object';$release.Tag=$data;$release.Icon=New-BlockMenuIcon 'Rotate'
$release.Add_Click({param($sender,$e);if($null-ne$sender.Tag.Target-and$null-ne$sender.Tag.Target.PSObject.Properties['ContinuousRotation']){$sender.Tag.Target.ContinuousRotation=$false;Set-PlacementAngle $sender.Tag.Target $sender.Tag.Target.Angle};$sender.Tag.Target=$null})
$menu.Items.Add($release)|Out-Null
}
$delete=New-Object Windows.Controls.MenuItem;$delete.Header="Delete $($data.Kind -creplace '([a-z])([A-Z])','$1 $2')";$delete.Tag=$data
$delete.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,116,126));$delete.Icon=New-BlockMenuIcon 'Delete' $delete.Foreground
$delete.Add_Click({param($sender,$e)Remove-PlayroomPlacement $sender.Tag});$menu.Items.Add($delete)|Out-Null
return $menu
}
function Initialize-RopeOverlay {
if($null-ne$script:ropeOverlayWindow){return}
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$width=[Windows.SystemParameters]::VirtualScreenWidth;$height=[Windows.SystemParameters]::VirtualScreenHeight
$script:ropeOverlayWindow=New-Object Windows.Window
$script:ropeOverlayWindow.Title='Windowisp Ropes'
$script:ropeOverlayWindow.Left=$left;$script:ropeOverlayWindow.Top=$top
$script:ropeOverlayWindow.Width=$width;$script:ropeOverlayWindow.Height=$height
$script:ropeOverlayWindow.WindowStyle=[Windows.WindowStyle]::None
$script:ropeOverlayWindow.AllowsTransparency=$true
$script:ropeOverlayWindow.Background=[Windows.Media.Brushes]::Transparent
$script:ropeOverlayWindow.ShowInTaskbar=$false;$script:ropeOverlayWindow.ShowActivated=$false
$script:ropeOverlayWindow.Topmost=$true;$script:ropeOverlayWindow.ResizeMode=[Windows.ResizeMode]::NoResize
$script:ropeOverlayCanvas=New-Object Windows.Controls.Canvas
$script:ropeOverlayCanvas.Width=$width;$script:ropeOverlayCanvas.Height=$height
$script:ropeOverlayWindow.Content=$script:ropeOverlayCanvas
$script:ropeOverlayWindow.Show()
Set-OverlayClickThroughState $script:ropeOverlayWindow $true
}
function Set-SharedOverlayElementPosition($element,[double]$x,[double]$y) {
if($null-eq$element){return}
[Windows.Controls.Canvas]::SetLeft($element,$x-[Windows.SystemParameters]::VirtualScreenLeft)
[Windows.Controls.Canvas]::SetTop($element,$y-[Windows.SystemParameters]::VirtualScreenTop)
}
function Add-SharedGameplayElement($element,[double]$x,[double]$y) {
Initialize-RopeOverlay
Set-SharedOverlayElementPosition $element $x $y
$script:ropeOverlayCanvas.Children.Add($element)|Out-Null
}
function Remove-SharedGameplayElement($element) {
if($null-ne$element-and$null-ne$script:ropeOverlayCanvas){$script:ropeOverlayCanvas.Children.Remove($element)|Out-Null}
}
function Get-RopeAttachmentPoint($data) {
if($null-eq$data){return $null}
$width=$(if($null-ne$data.PSObject.Properties['BodyWidth']){[double]$data.BodyWidth}elseif($null-ne$data.PSObject.Properties['Width']){[double]$data.Width}elseif($null-ne$data.PSObject.Properties['Size']){[double]$data.Size}else{40.0})
$height=$(if($null-ne$data.PSObject.Properties['BodyHeight']){[double]$data.BodyHeight}elseif($null-ne$data.PSObject.Properties['Height']){[double]$data.Height}elseif($null-ne$data.PSObject.Properties['Size']){[double]$data.Size}else{40.0})
$offsetX=$(if($null-ne$data.PSObject.Properties['BodyOffsetX']){[double]$data.BodyOffsetX}else{0.0})
$offsetY=$(if($null-ne$data.PSObject.Properties['BodyOffsetY']){[double]$data.BodyOffsetY}else{0.0})
$localY=$(if($script:playroomBalls.Contains($data)){0.0}else{-(($height/2)+4)})
$angle=$(if($null-ne$data.PSObject.Properties['Angle']){[double]$data.Angle}else{0.0})
$r=$angle*[Math]::PI/180
return [pscustomobject]@{
X=[double]$data.X+$offsetX+($width/2)-($localY*[Math]::Sin($r))
Y=[double]$data.Y+$offsetY+($height/2)+($localY*[Math]::Cos($r))
}
}
function Test-RopeDynamic($data) {
if($null-eq$data){return $false}
if($null-ne$data.PSObject.Properties['Dragging']-and$data.Dragging){return $false}
if($script:playroomBalls.Contains($data)){return -not$data.CarriedBy}
return $null-ne$data.PSObject.Properties['IsDynamic']-and$data.IsDynamic
}
function Start-RopePlacement {
Add-PlayroomGadget 'RopeCoil'
$script:ropeConnectSource=$null
$script:ropeConnectFirst=$null
if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='ROPE PLACED: RIGHT-CLICK THE COIL'}
}
function Arm-RopePlacement($coil) {
if($null-eq$coil-or$coil.Kind-ne'RopeCoil'){return}
$script:ropeConnectSource=$coil
$script:ropeConnectFirst=$null
if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='ROPE: LEFT-CLICK FIRST ITEM'}
}
function Connect-RopeEndpoint($data) {
if($null-eq$script:ropeConnectSource-or$null-eq$data-or$data-eq$script:ropeConnectSource-or$data.Kind-in@('Rope','RopeCoil')){return}
if($null-eq$script:ropeConnectFirst){
$script:ropeConnectFirst=$data
if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='ROPE: LEFT-CLICK SECOND ITEM'}
return
}
if($script:ropeConnectFirst-eq$data){
if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='ROPE: CHOOSE A DIFFERENT ITEM'}
return
}
New-PlayroomRope $script:ropeConnectFirst $data
$usedCoil=$script:ropeConnectSource
$script:ropeConnectSource=$null
$script:ropeConnectFirst=$null
Remove-PlayroomPlacement $usedCoil
if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='GADGET LAB'}
}
function Connect-RopeToolEndpoint($data) {
if($null-eq$data-or$data.Kind-in@('Rope','Wire')){return}
if($null-eq$script:ropeConnectFirst){$script:ropeConnectFirst=$data;if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='ROPE TOOL: CLICK SECOND ITEM'};return}
if($script:ropeConnectFirst-eq$data){return}
New-PlayroomRope $script:ropeConnectFirst $data
$script:ropeConnectFirst=$null
if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='ROPE TOOL: CLICK FIRST ITEM'}
}
function Test-WireInput($data){return $null-ne$data-and$data.Kind-in@('Switch','Button','PressurePlate','Timer')}
function Test-WireTarget($data){return $null-ne$data-and$data.Kind-in@('Spikes','FireJet','Laser','Fan')}
function Connect-WireEndpoint($data) {
if($null-eq$data-or(-not(Test-WireInput $data)-and-not(Test-WireTarget $data))){if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='WIRING: CHOOSE AN INPUT OR HAZARD'};return}
if($null-eq$script:wireConnectFirst){$script:wireConnectFirst=$data;if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='WIRING: CLICK THE OTHER END'};return}
if($script:wireConnectFirst-eq$data){return}
$source=$(if(Test-WireInput $script:wireConnectFirst){$script:wireConnectFirst}elseif(Test-WireInput $data){$data}else{$null})
$target=$(if(Test-WireTarget $script:wireConnectFirst){$script:wireConnectFirst}elseif(Test-WireTarget $data){$data}else{$null})
if($null-eq$source-or$null-eq$target){if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='WIRING NEEDS ONE INPUT + ONE HAZARD'};return}
New-PlayroomWire $source $target
$script:wireConnectFirst=$null
if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='WIRING: CONNECTED - CLICK NEXT INPUT OR HAZARD'}
}
function Get-WirePalette {
return @(
[Windows.Media.Color]::FromRgb(255,76,92),[Windows.Media.Color]::FromRgb(66,176,255),
[Windows.Media.Color]::FromRgb(80,224,137),[Windows.Media.Color]::FromRgb(255,205,65),
[Windows.Media.Color]::FromRgb(193,111,255),[Windows.Media.Color]::FromRgb(54,232,221)
)
}
function Set-WireChannel($wire,[int]$channel) {
if($null-eq$wire-or$wire.Kind-ne'Wire'){return}
$palette=Get-WirePalette;$wire.Channel=(($channel%$palette.Count)+$palette.Count)%$palette.Count;$wire.Colour=$palette[$wire.Channel]
$brush=New-Object Windows.Media.SolidColorBrush $wire.Colour
foreach($path in @($wire.ColourPaths)){$path.Stroke=$brush}
if($null-ne$wire.Element){$wire.Element.Background=$brush}
}
function New-PlayroomWire($source,$target) {
if($null-eq$source-or$null-eq$target){return}
Initialize-RopeOverlay
# A newly connected target follows its input immediately. Players can still
# override this later with Always On or Always Off from the context menu.
if($null-ne$target.PSObject.Properties['ManualPower']){$target.ManualPower=$null}
$palette=Get-WirePalette
$channel=$script:wireChannelIndex%$palette.Count;$script:wireChannelIndex++
$colour=$palette[$channel]
$glow=New-Object Windows.Shapes.Polyline;$glow.Stroke=New-Object Windows.Media.SolidColorBrush $colour;$glow.StrokeThickness=8;$glow.Opacity=.2
$outline=New-Object Windows.Shapes.Polyline;$outline.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(10,25,52));$outline.StrokeThickness=6;$outline.Opacity=.88
$line=New-Object Windows.Shapes.Polyline;$line.Stroke=New-Object Windows.Media.SolidColorBrush $colour;$line.StrokeThickness=3.2
$signal=New-Object Windows.Shapes.Polyline;$signal.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(238,252,255));$signal.StrokeThickness=1.5;$signal.Opacity=0
$signal.StrokeDashArray=New-Object Windows.Media.DoubleCollection;$signal.StrokeDashArray.Add(1.2);$signal.StrokeDashArray.Add(6.2)
foreach($path in @($glow,$outline,$line,$signal)){$path.StrokeLineJoin=[Windows.Media.PenLineJoin]::Round;$path.StrokeStartLineCap=[Windows.Media.PenLineCap]::Round;$path.StrokeEndLineCap=[Windows.Media.PenLineCap]::Round;$script:ropeOverlayCanvas.Children.Add($path)|Out-Null}
$handle=New-Object Windows.Window;$handle.Title='Windowisp Wire';$handle.Width=22;$handle.Height=22;$handle.WindowStyle='None';$handle.AllowsTransparency=$true;$handle.Background=[Windows.Media.Brushes]::Transparent;$handle.ShowInTaskbar=$false;$handle.ShowActivated=$false;$handle.Topmost=$true;$handle.ResizeMode='NoResize'
$dot=New-Object Windows.Controls.Border;$dot.Width=14;$dot.Height=14;$dot.CornerRadius=[Windows.CornerRadius]::new(7);$dot.Background=New-Object Windows.Media.SolidColorBrush $colour;$dot.BorderBrush=[Windows.Media.Brushes]::White;$dot.BorderThickness=[Windows.Thickness]::new(1)
$wire=[pscustomobject]@{Kind='Wire';Window=$handle;Element=$dot;Source=$source;Target=$target;Channel=$channel;Colour=$colour;VisualPaths=@($glow,$outline,$line,$signal);ColourPaths=@($glow,$line);SignalPath=$signal;Powered=$false;X=0.0;Y=0.0;Width=22.0;Height=22.0}
$dot.Tag=$wire;$handle.Tag=$wire
$menu=New-Object Windows.Controls.ContextMenu
$null=Set-WindowispContextMenuStyle $menu
$colourMenu=New-Object Windows.Controls.MenuItem;$colourMenu.Header='Wire Colour';$colourMenu.Icon=New-BlockMenuIcon 'Colour';Set-WindowispSubmenuStyle $colourMenu $menu.Background
$colourNames=@('Red','Blue','Green','Gold','Purple','Cyan')
for($colourIndex=0;$colourIndex-lt$palette.Count;$colourIndex++){
$colourItem=New-Object Windows.Controls.MenuItem;$colourItem.Header=$colourNames[$colourIndex]
Set-WindowispSubmenuStyle $colourItem $menu.Background
$colourItem.IsCheckable=$true;$colourItem.IsChecked=($wire.Channel-eq$colourIndex)
$colourItem.Icon=New-BlockMenuIcon 'Swatch' (New-Object Windows.Media.SolidColorBrush $palette[$colourIndex])
$colourItem.Tag=[pscustomobject]@{Wire=$wire;Channel=$colourIndex;Menu=$colourMenu}
$colourItem.Add_Click({param($s,$e)Set-WireChannel $s.Tag.Wire $s.Tag.Channel;foreach($item in @($s.Tag.Menu.Items)){if($item.IsCheckable){$item.IsChecked=($item-eq$s)}}})
$colourMenu.Items.Add($colourItem)|Out-Null
}
$menu.Items.Add($colourMenu)|Out-Null
$delete=New-Object Windows.Controls.MenuItem;$delete.Header='Delete Wire';$delete.Tag=$wire;$delete.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,116,126));$delete.Icon=New-BlockMenuIcon 'Delete' $delete.Foreground;$delete.Add_Click({param($s,$e)Remove-PlayroomPlacement $s.Tag});$menu.Items.Add($delete)|Out-Null;$dot.ContextMenu=$menu
Register-RopeSelectable $handle $wire
$handle.Content=$dot;$handle.Show();$script:playroomWires.Add($wire)|Out-Null
Update-OnePlayroomWire $wire
}
function Update-OnePlayroomWire($wire) {
if($null-eq$wire.Source-or$null-eq$wire.Target-or-not$wire.Source.Window.IsVisible-or-not$wire.Target.Window.IsVisible){return}
$a=Get-RopeAttachmentPoint $wire.Source;$b=Get-RopeAttachmentPoint $wire.Target
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$midX=($a.X+$b.X)/2;$midY=($a.Y+$b.Y)/2+18
$points=New-Object Windows.Media.PointCollection
$points.Add([Windows.Point]::new($a.X-$left,$a.Y-$top));$points.Add([Windows.Point]::new($midX-$left,$midY-$top));$points.Add([Windows.Point]::new($b.X-$left,$b.Y-$top))
foreach($path in @($wire.VisualPaths)){$path.Points=$points}
$wire.VisualPaths[0].Opacity=$(if($wire.Powered){.48}else{.12})
$wire.VisualPaths[1].Opacity=$(if($wire.Powered){.92}else{.58})
$wire.VisualPaths[2].Opacity=$(if($wire.Powered){1}else{.42})
$wire.SignalPath.Opacity=$(if($wire.Powered){.92}else{0});$wire.SignalPath.StrokeDashOffset=-1*($script:inputTick*1.15)
$wire.X=$midX-11;$wire.Y=$midY-11;$wire.Window.Left=$wire.X;$wire.Window.Top=$wire.Y
}
function Set-WireEditingVisible([bool]$visible) {
foreach($wire in @($script:playroomWires)){
foreach($path in @($wire.VisualPaths)){$path.Visibility=$(if($visible){[Windows.Visibility]::Visible}else{[Windows.Visibility]::Hidden})}
if($visible){$wire.Window.Show()}else{$wire.Window.Hide()}
}
}
function Set-RopeEditingVisible([bool]$visible) {
foreach($rope in @($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'})){
foreach($path in @($rope.VisualPaths)){$path.Visibility=$(if($visible){[Windows.Visibility]::Visible}else{[Windows.Visibility]::Hidden})}
if($visible){$rope.Window.Show()}else{$rope.Window.Hide()}
}
}
function Update-LogicGadgetVisual($logic) {
if($null-eq$logic-or$logic.Kind-notin@('Switch','Button','PressurePlate','Timer')){return}
$changed=[bool]$logic.SignalOn-ne[bool]$logic.PreviousSignalOn
if($changed){
$logic.PreviousSignalOn=[bool]$logic.SignalOn;$logic.StateAnimTicks=10
if($logic.Kind-eq'Switch'-and$null-ne$logic.LogicScale){$logic.LogicScale.ScaleX=$(if($logic.SignalOn){-1}else{1})}
}
if($null-ne$logic.LogicScale){
if($logic.Kind-eq'Button'){
$targetScale=$(if($logic.SignalOn){.84}else{1.0});$targetY=$(if($logic.SignalOn){4.5}else{0.0})
$logic.LogicScale.ScaleY+=($targetScale-$logic.LogicScale.ScaleY)*.32;$logic.LogicTranslate.Y+=($targetY-$logic.LogicTranslate.Y)*.32
}elseif($logic.Kind-eq'PressurePlate'){
$targetScale=$(if($logic.SignalOn){.68}else{1.0});$targetY=$(if($logic.SignalOn){6.0}else{0.0})
$logic.LogicScale.ScaleY+=($targetScale-$logic.LogicScale.ScaleY)*.38;$logic.LogicTranslate.Y+=($targetY-$logic.LogicTranslate.Y)*.38
}elseif($logic.Kind-eq'Switch'){
if($logic.StateAnimTicks-gt0){$progress=(10-$logic.StateAnimTicks)/10.0;$logic.LogicScale.ScaleY=1-(.055*[Math]::Sin([Math]::PI*$progress));$logic.StateAnimTicks--}
else{$logic.LogicScale.ScaleY=1}
}elseif($logic.Kind-eq'Timer'){
if(($script:inputTick%30)-eq0-and$logic.StateAnimTicks-le0){$logic.StateAnimTicks=10}
if($logic.StateAnimTicks-gt0){
$progress=(10-$logic.StateAnimTicks)/10.0;$pulse=[Math]::Sin([Math]::PI*$progress)
$logic.LogicScale.ScaleX=1+(.035*$pulse);$logic.LogicScale.ScaleY=1-(.075*$pulse);$logic.LogicTranslate.Y=2.2*$pulse;$logic.StateAnimTicks--
}else{$logic.LogicScale.ScaleX=1;$logic.LogicScale.ScaleY=1;$logic.LogicTranslate.Y=0}
}
}
if($null-ne$logic.Art){$logic.Art.Opacity=$(if($logic.SignalOn){1}else{.78})}
}
function Update-WiringSignals {
foreach($input in @($script:playroomGadgets|Where-Object{$_.Kind-in@('Switch','Button','PressurePlate','Timer')})){
if($input.Kind-eq'Button'){if($input.PulseTicks-gt0){$input.PulseTicks--};$input.SignalOn=$input.PulseTicks-gt0}
elseif($input.Kind-eq'Timer'){$input.SignalOn=(($script:inputTick%240)-lt120)}
elseif($input.Kind-eq'PressurePlate'){
$on=$script:x+$PetWidth-gt$input.X-and$script:x-lt$input.X+$input.Width-and$script:y+$PetHeight-gt$input.Y-and$script:y-lt$input.Y+$input.Height
if(-not$on-and$script:twoPlayerActive){$on=$script:p2X+$PetWidth-gt$input.X-and$script:p2X-lt$input.X+$input.Width-and$script:p2Y+$PetHeight-gt$input.Y-and$script:p2Y-lt$input.Y+$input.Height}
if(-not$on){foreach($o in @($script:playroomBalls)){if($o.X+$o.Size-gt$input.X-and$o.X-lt$input.X+$input.Width-and$o.Y+$o.Size-gt$input.Y-and$o.Y-lt$input.Y+$input.Height){$on=$true;break}}}
if(-not$on){
foreach($placement in @($script:playroomGadgets|Where-Object{$_-ne$input-and$null-ne$_.PSObject.Properties['IsDynamic']-and$_.IsDynamic})){
$bodyX=$placement.X+$(if($null-ne$placement.PSObject.Properties['BodyOffsetX']){$placement.BodyOffsetX}else{0})
$bodyY=$placement.Y+$(if($null-ne$placement.PSObject.Properties['BodyOffsetY']){$placement.BodyOffsetY}else{0})
$bodyW=$(if($null-ne$placement.PSObject.Properties['BodyWidth']){$placement.BodyWidth}else{$placement.Width})
$bodyH=$(if($null-ne$placement.PSObject.Properties['BodyHeight']){$placement.BodyHeight}else{$placement.Height})
if($bodyX+$bodyW-gt$input.X-and$bodyX-lt$input.X+$input.Width-and$bodyY+$bodyH-gt$input.Y-and$bodyY-lt$input.Y+$input.Height){$on=$true;break}
}
}
$input.SignalOn=$on
}
Update-LogicGadgetVisual $input
$input.Label.Text="$($input.Kind.ToUpperInvariant()) $(if($input.SignalOn){'ON'}else{'OFF'})"
}
foreach($wire in @($script:playroomWires)){
$wire.Powered=[bool]$wire.Source.SignalOn
# Gameplay keeps the electrical simulation, but its hidden editor wire
# no longer allocates a new PointCollection every frame.
if($script:playroomActive){Update-OnePlayroomWire $wire}
}
}
function Test-HazardWiredOn($hazard) {
if($null-ne$hazard.PSObject.Properties['ManualPower']-and$null-ne$hazard.ManualPower){return [bool]$hazard.ManualPower}
$connections=@($script:playroomWires|Where-Object{$_.Target-eq$hazard})
if($connections.Count-eq0){return $hazard.Kind-notin@('FireJet','Laser','Fan')}
return @($connections|Where-Object{$_.Powered}).Count-gt0
}
function Register-RopeSelectable($targetWindow,$data) {
if($null-eq$targetWindow-or$null-eq$data){return}
$targetWindow.Tag=$data
$targetWindow.Cursor=$(if($script:eraserMode){[Windows.Input.Cursors]::Cross}else{$null})
$targetWindow.Add_MouseEnter({
param($sender,$eventArgs)
if($script:eraserMode-and$null-ne$sender.Tag-and$null-ne$sender.Tag.PSObject.Properties['Element']){$sender.Tag.Element.Opacity=.48}
})
$targetWindow.Add_MouseLeave({
param($sender,$eventArgs)
if($null-ne$sender.Tag-and$null-ne$sender.Tag.PSObject.Properties['Element']){$sender.Tag.Element.Opacity=1}
})
$targetWindow.Add_PreviewMouseLeftButtonDown({
param($sender,$eventArgs)
if($script:eraserMode){
$target=$sender.Tag
if($null-ne$target){$target.Element.Opacity=1;Remove-PlayroomPlacement $target}
$eventArgs.Handled=$true
return
}
if($script:wiringMode){Connect-WireEndpoint $sender.Tag;$eventArgs.Handled=$true;return}
if($script:ropeToolMode){Connect-RopeToolEndpoint $sender.Tag;$eventArgs.Handled=$true;return}
if($null-ne$script:ropeConnectSource-and$sender.Tag-ne$script:ropeConnectSource){
Connect-RopeEndpoint $sender.Tag
$eventArgs.Handled=$true
}
})
}
function New-PlayroomRope($endA,$endB,[double]$requestedLength=0) {
if($null-eq$endA-or$null-eq$endB-or$endA-eq$endB){return}
Initialize-RopeOverlay
$frozenAnchor=$(if($endA.Kind-eq'Block'-and$endA.Type-eq'Ice'){$endA}elseif($endB.Kind-eq'Block'-and$endB.Type-eq'Ice'){$endB}else{$null})
$frozenOther=$(if($null-eq$frozenAnchor){$null}elseif($frozenAnchor-eq$endA){$endB}else{$endA})
$frozen=$null-ne$frozenAnchor
if($frozen-and$null-ne$frozenOther.PSObject.Properties['FrozenRope']-and$null-ne$frozenOther.FrozenRope){
# One frozen constraint owns an object at a time. Two frozen ropes trying
# to place the same device every frame causes snapping and can destabilise
# the WPF window updates.
Remove-PlayroomPlacement $frozenOther.FrozenRope
}
$frozenRadius=0.0;$frozenRelativeAngle=0.0;$frozenOtherAngleOffset=0.0
if($frozen){
$anchorCentreX=$frozenAnchor.X+($frozenAnchor.Width/2);$anchorCentreY=$frozenAnchor.Y+($frozenAnchor.Height/2)
$otherPoint=Get-RopeAttachmentPoint $frozenOther
$frozenDx=$otherPoint.X-$anchorCentreX;$frozenDy=$otherPoint.Y-$anchorCentreY
$frozenRadius=[Math]::Sqrt(($frozenDx*$frozenDx)+($frozenDy*$frozenDy))
$frozenRelativeAngle=([Math]::Atan2($frozenDy,$frozenDx)*180/[Math]::PI)-$frozenAnchor.Angle
if($null-ne$frozenOther.PSObject.Properties['Angle']){$frozenOtherAngleOffset=$frozenOther.Angle-$frozenAnchor.Angle}
}
$pointA=Get-RopeAttachmentPoint $endA;$pointB=Get-RopeAttachmentPoint $endB
$dx=$pointB.X-$pointA.X;$dy=$pointB.Y-$pointA.Y
$distance=[Math]::Sqrt(($dx*$dx)+($dy*$dy))
$ropeLength=$(if($requestedLength-gt0){$requestedLength}else{[Math]::Max(90,$distance*1.12)})
$ropeShadow=New-Object Windows.Shapes.Polyline
$ropeShadow.Stroke=New-Object Windows.Media.SolidColorBrush $(if($frozen){[Windows.Media.Color]::FromRgb(30,89,145)}else{[Windows.Media.Color]::FromRgb(66,38,26)})
$ropeShadow.StrokeThickness=8;$ropeShadow.StrokeLineJoin=[Windows.Media.PenLineJoin]::Round
# The wide dark stroke already supplies the rope's depth. A live blur on
# every moving segment forces an expensive off-screen render each update.
$ropeBody=New-Object Windows.Shapes.Polyline
$ropeBody.Stroke=New-Object Windows.Media.SolidColorBrush $(if($frozen){[Windows.Media.Color]::FromRgb(111,211,244)}else{[Windows.Media.Color]::FromRgb(213,145,55)})
$ropeBody.StrokeThickness=6;$ropeBody.StrokeLineJoin=[Windows.Media.PenLineJoin]::Round
$ropeHighlight=New-Object Windows.Shapes.Polyline
$ropeHighlight.Stroke=New-Object Windows.Media.SolidColorBrush $(if($frozen){[Windows.Media.Color]::FromRgb(233,253,255)}else{[Windows.Media.Color]::FromRgb(255,216,126)})
$ropeHighlight.StrokeThickness=2.2;$ropeHighlight.StrokeLineJoin=[Windows.Media.PenLineJoin]::Round
$ropeHighlight.StrokeDashArray=New-Object Windows.Media.DoubleCollection
$ropeHighlight.StrokeDashArray.Add(2.2);$ropeHighlight.StrokeDashArray.Add(2.0)
foreach($ropePath in @($ropeShadow,$ropeBody,$ropeHighlight)){$script:ropeOverlayCanvas.Children.Add($ropePath)|Out-Null}
$handleWindow=New-Object Windows.Window
$handleWindow.Title='Windowisp Rope Knot';$handleWindow.Width=26;$handleWindow.Height=26
$handleWindow.WindowStyle=[Windows.WindowStyle]::None;$handleWindow.AllowsTransparency=$true
$handleWindow.Background=[Windows.Media.Brushes]::Transparent;$handleWindow.ShowInTaskbar=$false
$handleWindow.ShowActivated=$false;$handleWindow.Topmost=$true;$handleWindow.ResizeMode=[Windows.ResizeMode]::NoResize
$knot=New-Object Windows.Controls.Border
$knot.Width=18;$knot.Height=18;$knot.CornerRadius=[Windows.CornerRadius]::new(9)
$knot.Background=New-Object Windows.Media.SolidColorBrush $(if($frozen){[Windows.Media.Color]::FromRgb(64,166,221)}else{[Windows.Media.Color]::FromRgb(190,126,52)})
$knot.BorderBrush=New-Object Windows.Media.SolidColorBrush $(if($frozen){[Windows.Media.Color]::FromRgb(225,252,255)}else{[Windows.Media.Color]::FromRgb(248,210,121)})
$knot.BorderThickness=[Windows.Thickness]::new(2);$knot.Cursor=[Windows.Input.Cursors]::Hand
$ropePoints=New-Object Windows.Media.PointCollection
$rope=[pscustomobject]@{Kind='Rope';Window=$handleWindow;Element=$knot;VisualPath=$ropeBody;VisualPaths=@($ropeShadow,$ropeBody,$ropeHighlight);CachedPoints=$ropePoints;EndA=$endA;EndB=$endB;Length=[double]$ropeLength;Frozen=$frozen;FrozenAnchor=$frozenAnchor;FrozenOther=$frozenOther;FrozenRadius=[double]$frozenRadius;FrozenRelativeAngle=[double]$frozenRelativeAngle;FrozenOtherAngleOffset=[double]$frozenOtherAngleOffset;FrozenLastAnchorAngle=$(if($frozen){[double]$frozenAnchor.Angle}else{0.0});VisualPhase=($script:playroomGadgets.Count%2);ShadowVisible=$true;FineHighlightVisible=$true;LastVisualAX=[double]::NaN;LastVisualAY=[double]::NaN;LastVisualBX=[double]::NaN;LastVisualBY=[double]::NaN;LastVisualLength=[double]::NaN;LastVisualTick=-100;LastHandleX=[double]::NaN;LastHandleY=[double]::NaN;X=0.0;Y=0.0;Width=26.0;Height=26.0;Angle=0.0;DebugSeen=$false;Dragging=$false;DragStartLength=0.0;DragStartFrozenRadius=0.0;DragStartX=0.0;DragStartY=0.0}
foreach($ropePath in @($rope.VisualPaths)){$ropePath.Points=$ropePoints}
$knot.Tag=$rope;$handleWindow.Tag=$rope
$knot.ToolTip=$(if($frozen){'Frozen rope handle - drag up/down to change its reach'}else{'Rope handle - drag up/down to change its length'})
if($frozen){$knot.Width=22;$knot.Height=22;$knot.CornerRadius=[Windows.CornerRadius]::new(11);$knot.Effect=New-Object Windows.Media.Effects.DropShadowEffect;$knot.Effect.Color=[Windows.Media.Color]::FromRgb(116,225,255);$knot.Effect.BlurRadius=12;$knot.Effect.ShadowDepth=0;$knot.Effect.Opacity=.9}
$knot.ContextMenu=New-PlacementContextMenu $rope $false
Register-RopeSelectable $handleWindow $rope
$knot.Add_MouseLeftButtonDown({
param($sender,$eventArgs)
if(-not$script:playroomActive){return}
$sender.Tag.DragStartLength=$sender.Tag.Length
$sender.Tag.DragStartFrozenRadius=$sender.Tag.FrozenRadius
$sender.Tag.DragStartX=$sender.Tag.Window.Left+($sender.Tag.Window.Width/2)
$sender.Tag.DragStartY=$sender.Tag.Window.Top+($sender.Tag.Window.Height/2)
$sender.Tag.Dragging=$true
try{$sender.Tag.Window.DragMove()}finally{
Set-RopeLengthFromHandle $sender.Tag
$sender.Tag.Dragging=$false
Update-OnePlayroomRope $sender.Tag $true
}
$eventArgs.Handled=$true
})
$handleWindow.Add_LocationChanged({
param($sender,$eventArgs)
if($null-ne$sender.Tag-and$sender.Tag.Dragging){Set-RopeLengthFromHandle $sender.Tag;Update-OnePlayroomRope $sender.Tag $true}
})
$handleWindow.Content=$knot;$handleWindow.Show()
$script:playroomGadgets.Add($rope)|Out-Null
if($frozen){
if($null-eq$frozenOther.PSObject.Properties['FrozenRope']){$frozenOther|Add-Member -NotePropertyName FrozenRope -NotePropertyValue $rope}
else{$frozenOther.FrozenRope=$rope}
if($null-ne$frozenOther.PSObject.Properties['CarriedBy']){$frozenOther.CarriedBy=''}
}
Update-OnePlayroomRope $rope $true
}
function Set-RopeLengthFromHandle($rope) {
if($null-eq$rope-or$rope.Kind-ne'Rope'){return}
$a=Get-RopeAttachmentPoint $rope.EndA;$b=Get-RopeAttachmentPoint $rope.EndB
if($null-eq$a-or$null-eq$b){return}
$handleX=$rope.Window.Left+($rope.Window.Width/2);$handleY=$rope.Window.Top+($rope.Window.Height/2)
$maxLength=[Math]::Sqrt(([Windows.SystemParameters]::VirtualScreenWidth*[Windows.SystemParameters]::VirtualScreenWidth)+([Windows.SystemParameters]::VirtualScreenHeight*[Windows.SystemParameters]::VirtualScreenHeight))*2
if($rope.Frozen-and$rope.DragStartFrozenRadius-gt0){
$verticalChange=$handleY-$rope.DragStartY
$sideChange=[Math]::Abs($handleX-$rope.DragStartX)
$rope.FrozenRadius=[Math]::Min($maxLength,[Math]::Max(28,$rope.DragStartFrozenRadius+($verticalChange*2)+($sideChange*.25)))
return
}
if($rope.DragStartLength-gt0){
$verticalChange=$handleY-$rope.DragStartY
$sideChange=[Math]::Abs($handleX-$rope.DragStartX)
$rope.Length=[Math]::Min($maxLength,[Math]::Max(40,$rope.DragStartLength+($verticalChange*2)+($sideChange*.25)))
}else{
$aDx=$handleX-$a.X;$aDy=$handleY-$a.Y;$bDx=$b.X-$handleX;$bDy=$b.Y-$handleY
$throughHandle=[Math]::Sqrt(($aDx*$aDx)+($aDy*$aDy))+[Math]::Sqrt(($bDx*$bDx)+($bDy*$bDy))
$rope.Length=[Math]::Min($maxLength,[Math]::Max(40,$throughHandle))
}
}
function Unattach-PlayroomRope($rope) {
if($null-eq$rope-or$rope.Kind-ne'Rope'){return}
$coilCenterX=$rope.X+($rope.Width/2);$coilCenterY=$rope.Y+($rope.Height/2)
Add-PlayroomGadget 'RopeCoil'
$coil=@($script:playroomGadgets|Where-Object{$_.Kind-eq'RopeCoil'}|Select-Object -Last 1)[0]
if($null-ne$coil){
$coil.X=$coilCenterX-($coil.Width/2);$coil.Y=$coilCenterY-($coil.Height/2)
$coil.Window.Left=$coil.X;$coil.Window.Top=$coil.Y
}
Remove-PlayroomPlacement $rope
if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='ROPE UNATTACHED'}
}
function Remove-RopesForPlacement($data) {
foreach($rope in @($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'-and($_.EndA-eq$data-or$_.EndB-eq$data)})){
Remove-PlayroomPlacement $rope
}
if($script:ropeConnectFirst-eq$data){$script:ropeConnectFirst=$null}
if($script:ropeConnectSource-eq$data){
$script:ropeConnectSource=$null;$script:ropeConnectFirst=$null
if($null-ne$script:sandboxTrayTitle){$script:sandboxTrayTitle.Text='GADGET LAB'}
}
}
function Move-RopeDynamicObject($data,[double]$moveX,[double]$moveY,[double]$normalX,[double]$normalY) {
$data.X+=$moveX;$data.Y+=$moveY
$outward=($data.VX*$normalX)+($data.VY*$normalY)
if($outward-gt0){$data.VX-=$outward*$normalX;$data.VY-=$outward*$normalY}
# Balls and dynamic devices have already drawn once in their own physics
# pass this tick. Updating their top-level WPF window again here was the
# dominant multi-rope cost. Keep physics coordinates exact immediately and
# let the normal object pass present the correction on the following tick.
}
function Set-PlacementWindowPosition($data) {
if($null-eq$data-or$null-eq$data.Window){return}
$logicalWidth=$(if($null-ne$data.PSObject.Properties['Width']){[double]$data.Width}else{[double]$data.Window.Width})
$logicalHeight=$(if($null-ne$data.PSObject.Properties['Height']){[double]$data.Height}else{[double]$data.Window.Height})
# Capture both coordinates before moving either window axis. WPF raises
# LocationChanged after Left is assigned; gadget handlers can synchronise
# X/Y immediately and would otherwise overwrite the pending Y destination.
$logicalX=[double]$data.X;$logicalY=[double]$data.Y
$targetLeft=$logicalX-(($data.Window.Width-$logicalWidth)/2)
$targetTop=$logicalY-(($data.Window.Height-$logicalHeight)/2)
$data.Window.Left=$targetLeft;$data.Window.Top=$targetTop
$data.X=$logicalX;$data.Y=$logicalY
}
function Update-DynamicHazards {
if(-not$script:playroomActive-and-not$script:active){return}
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$right=$left+[Windows.SystemParameters]::VirtualScreenWidth;$floor=$top+[Windows.SystemParameters]::VirtualScreenHeight
foreach($hazard in @($script:playroomGadgets|Where-Object{$null-ne$_.PSObject.Properties['IsDynamic']-and$_.IsDynamic})){
if($null-ne$hazard.PSObject.Properties['FrozenRope']-and$null-ne$hazard.FrozenRope){continue}
# A rotator acts as a temporary physical anchor. Without this guard the
# gravity solver and rotator would compete for ownership every frame.
$rotatorOwner=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Rotator'-and$_.Target-eq$hazard}|Select-Object -First 1)
if($rotatorOwner.Count-gt0){$hazard.VX=0;$hazard.VY=0;continue}
if($hazard.Dragging){$hazard.VX=0;$hazard.VY=0;continue}
if($hazard.SpringCooldown-gt0){$hazard.SpringCooldown--}
$bodyW=[double]$hazard.BodyWidth;$bodyH=[double]$hazard.BodyHeight
$offX=[double]$hazard.BodyOffsetX;$offY=[double]$hazard.BodyOffsetY
$oldBodyX=$hazard.X+$offX;$oldBodyY=$hazard.Y+$offY
# Gravity is unconditional. A valid surface contact may resolve it below,
# but no remembered conveyor/rope state can suspend a free hazard.
$hazard.VY=[Math]::Min(18,$hazard.VY+.82)
$hazard.VX*=.985
$hazard.X+=$hazard.VX;$hazard.Y+=$hazard.VY
$bodyX=$hazard.X+$offX;$bodyY=$hazard.Y+$offY
if($bodyX-lt$left){$hazard.X=$left-$offX;$hazard.VX=[Math]::Abs($hazard.VX)*.35}
if($bodyX+$bodyW-gt$right){$hazard.X=$right-$bodyW-$offX;$hazard.VX=-[Math]::Abs($hazard.VX)*.35}
if($bodyY-lt$top){$hazard.Y=$top-$offY;$hazard.VY=[Math]::Abs($hazard.VY)*.3}
if($bodyY+$bodyH-gt$floor){
$impact=[Math]::Abs($hazard.VY);$hazard.Y=$floor-$bodyH-$offY
$hazard.VY=$(if($impact-lt2.2){0.0}else{-$impact*.22});$hazard.VX*=.88
}
foreach($block in @($script:playroomBlocks)){
if($block.Type-eq'Ladder'-or$block.TemporaryState-eq'Gone'){continue}
$bodyX=$hazard.X+$offX;$bodyY=$hazard.Y+$offY
if([Math]::Abs($block.Angle)-gt.01){
# Sweep along the whole frame path. This is the same anti-tunnel
# contract used by balls, and is especially important for thin
# rotating platforms and the compact Fire Jet body.
$targetX=$hazard.X;$targetY=$hazard.Y
$travel=[Math]::Max([Math]::Abs(($targetX+$offX)-$oldBodyX),[Math]::Abs(($targetY+$offY)-$oldBodyY))
$sweepSteps=[Math]::Max(1,[Math]::Min(10,[Math]::Ceiling($travel/4.0)))
$rotatedHit=$false
for($sweepStep=1;$sweepStep-le$sweepSteps;$sweepStep++){
$sweepT=$sweepStep/[double]$sweepSteps
$hazard.X=($oldBodyX-$offX)+(($targetX-($oldBodyX-$offX))*$sweepT)
$hazard.Y=($oldBodyY-$offY)+(($targetY-($oldBodyY-$offY))*$sweepT)
if(Resolve-DynamicPlacementRotatedBlock $hazard $block .2){$rotatedHit=$true;break}
}
if(-not$rotatedHit){$hazard.X=$targetX;$hazard.Y=$targetY}
else{break}
continue
}
if([Math]::Abs($block.Angle)-le.01-and$null-ne$block.Window-and$block.Window.IsVisible){
# For a flat block, collide with its real on-screen window. Dragging
# and resizing can briefly desynchronise stored logical dimensions;
# using them created invisible conveyor extensions for hazards.
$blockLeft=[double]$block.Window.Left;$blockTop=[double]$block.Window.Top
$visibleWidth=$(if($block.Window.ActualWidth-gt0){$block.Window.ActualWidth}else{$block.Window.Width})
$visibleHeight=$(if($block.Window.ActualHeight-gt0){$block.Window.ActualHeight}else{$block.Window.Height})
$blockRight=$blockLeft+$visibleWidth;$blockBottom=$blockTop+$visibleHeight
}else{
$blockLeft=$block.X;$blockRight=$block.X+$block.Width;$blockTop=$block.Y;$blockBottom=$block.Y+$block.Height
}
$overlapX=$bodyX+$bodyW-gt$blockLeft-and$bodyX-lt$blockRight
$overlapY=$bodyY+$bodyH-gt$blockTop-and$bodyY-lt$blockBottom
# Use the physical centre as the balance point. The instant it passes
# a platform edge, support ends and gravity takes over; long hazard art
# can no longer hang almost entirely in mid-air.
$bodyCentreX=$bodyX+($bodyW/2)
$hasTopSupport=$bodyCentreX-ge$blockLeft-and$bodyCentreX-le$blockRight
if(-not$overlapX-or-not$overlapY){continue}
if($hasTopSupport-and$oldBodyY+$bodyH-le$blockTop+5-and$hazard.VY-ge0){
$impact=[Math]::Abs($hazard.VY);$hazard.Y=$blockTop-$bodyH-$offY
Trigger-TemporaryBlock $block ("Placement:{0}" -f [Runtime.CompilerServices.RuntimeHelpers]::GetHashCode($hazard))
$hazard.VY=$(if($block.Type-eq'Bouncy'){-[Math]::Max(9,$impact*.75)}elseif($impact-lt2){0}else{-$impact*.2})
if($block.Type-eq'Ice'){$hazard.VX*=1.04}else{$hazard.VX*=.94}
if($block.Type-eq'Conveyor'){
$angle=$block.Angle*[Math]::PI/180
$targetConveyorVX=[Math]::Cos($angle)*$block.Direction*5.5
$targetConveyorVY=[Math]::Sin($angle)*$block.Direction*5.5
$hazard.VX+=($targetConveyorVX-$hazard.VX)*.22
$hazard.VY+=($targetConveyorVY-$hazard.VY)*.22
}
# A dynamic device can only be supported by one top surface in a
# frame. Continuing through overlapping platforms caused competing
# conveyor impulses and visible left/right oscillation.
break
}elseif($block.Type-eq'Cloud'){
# Cloud bodies have no underside or side walls. Dynamic
# placements can rise or drift through them and only settle on
# the top face while descending.
continue
}elseif($oldBodyY-ge$blockBottom-5-and$hazard.VY-lt0){
$hazard.Y=$blockBottom-$offY;$hazard.VY=[Math]::Abs($hazard.VY)*.22
}elseif($oldBodyX+$bodyW-le$blockLeft+5-and$hazard.VX-gt0){
$hazard.X=$blockLeft-$bodyW-$offX;$hazard.VX=-[Math]::Abs($hazard.VX)*.3
}elseif($oldBodyX-ge$blockRight-5-and$hazard.VX-lt0){
$hazard.X=$blockRight-$offX;$hazard.VX=[Math]::Abs($hazard.VX)*.3
}
}
Set-PlacementWindowPosition $hazard
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_CONVEYOR_HAZARD-eq'1'-and$hazard.Kind-eq'FireJet'-and($script:hazardTick%15)-eq0){
[Console]::Out.WriteLine("diagnostic:conveyor-hazard x=$([Math]::Round($hazard.X,1)) y=$([Math]::Round($hazard.Y,1)) vx=$([Math]::Round($hazard.VX,2)) vy=$([Math]::Round($hazard.VY,2))")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_HAZARD_GRAVITY-eq'1'-and-not$hazard.PhysicsDebugSeen-and[Math]::Abs($hazard.Y-$hazard.PhysicsStartY)-gt12){
[Console]::Out.WriteLine("diagnostic:hazard-gravity kind=$($hazard.Kind) y=$($hazard.Y) vy=$($hazard.VY)")
$hazard.PhysicsDebugSeen=$true
}
}
}
function Resolve-RopePointAgainstBlock([double]$pointX,[double]$pointY,$block) {
$r=$block.Angle*[Math]::PI/180
$ux=[Math]::Cos($r);$uy=[Math]::Sin($r);$vx=-[Math]::Sin($r);$vy=[Math]::Cos($r)
$centreX=$block.X+($block.Width/2);$centreY=$block.Y+($block.Height/2)
$dx=$pointX-$centreX;$dy=$pointY-$centreY
$localX=($dx*$ux)+($dy*$uy);$localY=($dx*$vx)+($dy*$vy)
$halfW=($block.Width/2)+4;$halfH=($block.Height/2)+4
if([Math]::Abs($localX)-ge$halfW-or[Math]::Abs($localY)-ge$halfH){
return [pscustomobject]@{X=$pointX;Y=$pointY;Hit=$false}
}
$edgeDistances=@(
[pscustomobject]@{Distance=$localX+$halfW;Edge='Left'},
[pscustomobject]@{Distance=$halfW-$localX;Edge='Right'},
[pscustomobject]@{Distance=$localY+$halfH;Edge='Top'},
[pscustomobject]@{Distance=$halfH-$localY;Edge='Bottom'}
)
$nearest=@($edgeDistances|Sort-Object Distance|Select-Object -First 1)[0]
if($nearest.Edge-eq'Left'){$localX=-$halfW}
elseif($nearest.Edge-eq'Right'){$localX=$halfW}
elseif($nearest.Edge-eq'Top'){$localY=-$halfH}
else{$localY=$halfH}
return [pscustomobject]@{
X=$centreX+($localX*$ux)+($localY*$vx)
Y=$centreY+($localX*$uy)+($localY*$vy)
Hit=$true
}
}
function Update-FrozenRope($rope) {
if($null-eq$rope-or-not$rope.Frozen-or$null-eq$rope.FrozenAnchor-or$null-eq$rope.FrozenOther){return}
$anchor=$rope.FrozenAnchor;$other=$rope.FrozenOther
if($null-ne$other.PSObject.Properties['Dragging']-and$other.Dragging){return}
$anchorCentreX=$anchor.X+($anchor.Width/2);$anchorCentreY=$anchor.Y+($anchor.Height/2)
$worldAngle=$anchor.Angle+$rope.FrozenRelativeAngle
$worldRadians=$worldAngle*[Math]::PI/180
$targetX=$anchorCentreX+([Math]::Cos($worldRadians)*$rope.FrozenRadius)
$targetY=$anchorCentreY+([Math]::Sin($worldRadians)*$rope.FrozenRadius)
$desiredOtherAngle=$anchor.Angle+$rope.FrozenOtherAngleOffset
if($script:playroomBalls.Contains($other)){
if([Math]::Abs($other.Angle-$desiredOtherAngle)-gt.01){
$other.Angle=$desiredOtherAngle
Set-PlayroomObjectVisual $other $other.Angle
}
}elseif($null-ne$other.PSObject.Properties['Angle']-and[Math]::Abs($other.Angle-$desiredOtherAngle)-gt.01){
Set-PlacementAngle $other $desiredOtherAngle
}
$currentPoint=Get-RopeAttachmentPoint $other
$moveX=$targetX-$currentPoint.X;$moveY=$targetY-$currentPoint.Y
if([Math]::Abs($moveX)-gt.001-or[Math]::Abs($moveY)-gt.001){$other.X+=$moveX;$other.Y+=$moveY}
if($null-ne$other.PSObject.Properties['VX']){$other.VX=$moveX}
if($null-ne$other.PSObject.Properties['VY']){$other.VY=$moveY}
if([Math]::Abs($moveX)-gt.001-or[Math]::Abs($moveY)-gt.001){Set-PlacementWindowPosition $other}
$rope.FrozenLastAnchorAngle=$anchor.Angle
}
function Rebase-FrozenRopeForPlacement($placement) {
if($null-eq$placement){return}
foreach($rope in @($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'-and$_.Frozen-and$_.FrozenOther-eq$placement})){
$anchor=$rope.FrozenAnchor
if($null-eq$anchor-or$null-eq$anchor.Window-or-not$anchor.Window.IsVisible){continue}
$anchorCentreX=$anchor.X+($anchor.Width/2);$anchorCentreY=$anchor.Y+($anchor.Height/2)
$point=Get-RopeAttachmentPoint $placement
if($null-eq$point){continue}
$dx=$point.X-$anchorCentreX;$dy=$point.Y-$anchorCentreY
$radius=[Math]::Sqrt(($dx*$dx)+($dy*$dy))
if([double]::IsNaN($radius)-or[double]::IsInfinity($radius)){continue}
$rope.FrozenRadius=[Math]::Max(28,$radius)
$rope.FrozenRelativeAngle=([Math]::Atan2($dy,$dx)*180/[Math]::PI)-$anchor.Angle
if($null-ne$placement.PSObject.Properties['Angle']){$rope.FrozenOtherAngleOffset=$placement.Angle-$anchor.Angle}
$rope.FrozenLastAnchorAngle=$anchor.Angle
$rope.Length=$rope.FrozenRadius
Update-OnePlayroomRope $rope $true
}
}
function Update-OnePlayroomRope($rope,[bool]$forceVisual=$false) {
if($null-eq$rope.EndA-or$null-eq$rope.EndB-or-not$rope.EndA.Window.IsVisible-or-not$rope.EndB.Window.IsVisible){return}
if($rope.Frozen){Update-FrozenRope $rope}
$a=Get-RopeAttachmentPoint $rope.EndA;$b=Get-RopeAttachmentPoint $rope.EndB
$dx=$b.X-$a.X;$dy=$b.Y-$a.Y;$distance=[Math]::Sqrt(($dx*$dx)+($dy*$dy))
if($distance-gt$rope.Length-and$distance-gt.001){
$nx=$dx/$distance;$ny=$dy/$distance;$excess=$distance-$rope.Length
$dynamicA=Test-RopeDynamic $rope.EndA;$dynamicB=Test-RopeDynamic $rope.EndB
if($dynamicA-and$dynamicB){
$moveAX=$nx*$excess*.5;$moveAY=$ny*$excess*.5;$moveBX=-$moveAX;$moveBY=-$moveAY
Move-RopeDynamicObject $rope.EndA $moveAX $moveAY (-$nx) (-$ny)
Move-RopeDynamicObject $rope.EndB $moveBX $moveBY $nx $ny
$a.X+=$moveAX;$a.Y+=$moveAY;$b.X+=$moveBX;$b.Y+=$moveBY
}elseif($dynamicA){
$moveAX=$nx*$excess;$moveAY=$ny*$excess
Move-RopeDynamicObject $rope.EndA $moveAX $moveAY (-$nx) (-$ny)
$a.X+=$moveAX;$a.Y+=$moveAY
}elseif($dynamicB){
$moveBX=-$nx*$excess;$moveBY=-$ny*$excess
Move-RopeDynamicObject $rope.EndB $moveBX $moveBY $nx $ny
$b.X+=$moveBX;$b.Y+=$moveBY
}
$dx=$b.X-$a.X;$dy=$b.Y-$a.Y;$distance=[Math]::Sqrt(($dx*$dx)+($dy*$dy))
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_ROPE-eq'1'-and-not$rope.DebugSeen){
[Console]::Out.WriteLine("diagnostic:rope-constrained distance=$distance length=$($rope.Length)")
$rope.DebugSeen=$true
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_HAZARD_GRAVITY-eq'1'-and-not$rope.DebugSeen-and($rope.EndA.Kind-in@('Spikes','Laser','FireJet')-or$rope.EndB.Kind-in@('Spikes','Laser','FireJet'))){
[Console]::Out.WriteLine("diagnostic:hazard-rope-constrained distance=$distance length=$($rope.Length)")
$rope.DebugSeen=$true
}
}
# The rope constraint remains active in gameplay, while its hidden editor
# curve and obstacle-routing geometry are suspended completely.
if(-not$script:playroomActive-and-not$forceVisual){return}
# Physics constraints remain full-rate; curve allocation, block routing and
# WPF PointCollection replacement are staggered across alternating frames.
if(-not$forceVisual-and-not$rope.Dragging-and(($script:ropeVisualTick+[int]$rope.VisualPhase)%$script:ropeVisualDivisor)-ne0){return}
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
# Tiny endpoint changes do not warrant rebuilding three WPF polylines. A
# periodic refresh still picks up a block being dragged into a resting rope.
if(-not$forceVisual-and-not$rope.Dragging-and-not[double]::IsNaN($rope.LastVisualAX)){
$visualDelta=[Math]::Max([Math]::Max([Math]::Abs($a.X-$rope.LastVisualAX),[Math]::Abs($a.Y-$rope.LastVisualAY)),[Math]::Max([Math]::Abs($b.X-$rope.LastVisualBX),[Math]::Abs($b.Y-$rope.LastVisualBY)))
if($visualDelta-lt1.25-and[Math]::Abs($rope.Length-$rope.LastVisualLength)-lt.5-and($script:ropeVisualTick-$rope.LastVisualTick)-lt8){return}
}
if($rope.Frozen){
$rope.Length=$distance
# Frozen ropes are rigid straight links. Three points are visually
# identical to the old 21+ point curve and avoid needless routing work.
$points=$rope.CachedPoints;$points.Clear()
$points.Add([Windows.Point]::new($a.X-$left,$a.Y-$top))
$points.Add([Windows.Point]::new((($a.X+$b.X)/2)-$left,(($a.Y+$b.Y)/2)-$top))
$points.Add([Windows.Point]::new($b.X-$left,$b.Y-$top))
if(-not$rope.Dragging){
$rope.X=(($a.X+$b.X)/2)-13;$rope.Y=(($a.Y+$b.Y)/2)-13
if([double]::IsNaN($rope.LastHandleX)-or[Math]::Abs($rope.X-$rope.LastHandleX)-ge1-or[Math]::Abs($rope.Y-$rope.LastHandleY)-ge1){$rope.Window.Left=$rope.X;$rope.Window.Top=$rope.Y;$rope.LastHandleX=$rope.X;$rope.LastHandleY=$rope.Y}
}
$rope.LastVisualAX=$a.X;$rope.LastVisualAY=$a.Y;$rope.LastVisualBX=$b.X;$rope.LastVisualBY=$b.Y;$rope.LastVisualLength=$rope.Length;$rope.LastVisualTick=$script:ropeVisualTick
return
}else{
$geometricSag=[Math]::Sqrt([Math]::Max(0,(($rope.Length/2)*($rope.Length/2))-(($distance/2)*($distance/2))))
$sag=[Math]::Min([Windows.SystemParameters]::VirtualScreenHeight*.42,10+($geometricSag*.75))
}
# Twelve to thirty-two segments remain visually smooth at desktop scale,
# while substantially reducing Point creation and per-block routing work.
$sampleCount=[Math]::Max(12,[Math]::Min(32,[Math]::Ceiling([Math]::Max($distance,$rope.Length)/28)))
$routeBlocks=New-Object Collections.ArrayList
$routeMinX=[Math]::Min($a.X,$b.X)-8;$routeMaxX=[Math]::Max($a.X,$b.X)+8
$routeMinY=[Math]::Min($a.Y,$b.Y)-8;$routeMaxY=[Math]::Max($a.Y,$b.Y)+$sag+8
foreach($block in @($script:playroomBlocks)){
if($block-eq$rope.EndA-or$block-eq$rope.EndB-or$block.Type-eq'Ladder'-or$block.TemporaryState-eq'Gone'){continue}
# A half-diagonal bound safely covers rotated blocks without invoking
# the more expensive exact point/rectangle resolver for distant blocks.
$blockRadius=([Math]::Sqrt(($block.Width*$block.Width)+($block.Height*$block.Height))/2)+4
$blockCentreX=$block.X+($block.Width/2);$blockCentreY=$block.Y+($block.Height/2)
if($blockCentreX+$blockRadius-lt$routeMinX-or$blockCentreX-$blockRadius-gt$routeMaxX-or$blockCentreY+$blockRadius-lt$routeMinY-or$blockCentreY-$blockRadius-gt$routeMaxY){continue}
$routeBlocks.Add($block)|Out-Null
}
$absolutePoints=New-Object Collections.ArrayList
$basePathLength=0.0;$previousBase=$null;$measureRoute=$routeBlocks.Count-gt0
for($i=0;$i-le$sampleCount;$i++){
$t=$i/[double]$sampleCount;$curve=4*$t*(1-$t)
$basePoint=[Windows.Point]::new($a.X+($dx*$t),$a.Y+($dy*$t)+($sag*$curve))
if($measureRoute-and$null-ne$previousBase){$bdx=$basePoint.X-$previousBase.X;$bdy=$basePoint.Y-$previousBase.Y;$basePathLength+=[Math]::Sqrt(($bdx*$bdx)+($bdy*$bdy))}
$previousBase=$basePoint
$absolutePoints.Add($basePoint)|Out-Null
}
$ropeTouchesBlock=$false
for($pointIndex=1;$pointIndex-lt$absolutePoints.Count-1;$pointIndex++){
$current=$absolutePoints[$pointIndex]
if($rope.Frozen){$absolutePoints[$pointIndex]=$current;continue}
foreach($block in @($routeBlocks)){
$resolvedPoint=Resolve-RopePointAgainstBlock $current.X $current.Y $block
if($resolvedPoint.Hit){
$current=[Windows.Point]::new($resolvedPoint.X,$resolvedPoint.Y)
$ropeTouchesBlock=$true
}
}
$absolutePoints[$pointIndex]=$current
}
$points=$rope.CachedPoints;$points.Clear()
$routedLength=0.0;$previousPoint=$null
foreach($absolutePoint in @($absolutePoints)){
if($null-ne$previousPoint){$pdx=$absolutePoint.X-$previousPoint.X;$pdy=$absolutePoint.Y-$previousPoint.Y;$routedLength+=[Math]::Sqrt(($pdx*$pdx)+($pdy*$pdy))}
$previousPoint=$absolutePoint
$points.Add([Windows.Point]::new($absolutePoint.X-$left,$absolutePoint.Y-$top))
}
if($ropeTouchesBlock-and$env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_ROPE_BLOCK_COLLISION-eq'1'-and-not$rope.DebugSeen){
[Console]::Out.WriteLine("diagnostic:rope-block-contact base=$basePathLength routed=$routedLength length=$($rope.Length)")
$rope.DebugSeen=$true
}
if($ropeTouchesBlock-and$routedLength-gt$rope.Length+1){
$routeExcess=$routedLength-$rope.Length
$dynamicA=Test-RopeDynamic $rope.EndA;$dynamicB=Test-RopeDynamic $rope.EndB
$first=$absolutePoints[0];$next=$absolutePoints[1]
$last=$absolutePoints[$absolutePoints.Count-1];$prior=$absolutePoints[$absolutePoints.Count-2]
$aDx=$next.X-$first.X;$aDy=$next.Y-$first.Y;$aDistance=[Math]::Max(.001,[Math]::Sqrt(($aDx*$aDx)+($aDy*$aDy)))
$bDx=$prior.X-$last.X;$bDy=$prior.Y-$last.Y;$bDistance=[Math]::Max(.001,[Math]::Sqrt(($bDx*$bDx)+($bDy*$bDy)))
$aNx=$aDx/$aDistance;$aNy=$aDy/$aDistance;$bNx=$bDx/$bDistance;$bNy=$bDy/$bDistance
if($dynamicA-and$dynamicB){
Move-RopeDynamicObject $rope.EndA ($aNx*$routeExcess*.5) ($aNy*$routeExcess*.5) (-$aNx) (-$aNy)
Move-RopeDynamicObject $rope.EndB ($bNx*$routeExcess*.5) ($bNy*$routeExcess*.5) (-$bNx) (-$bNy)
}elseif($dynamicA){Move-RopeDynamicObject $rope.EndA ($aNx*$routeExcess) ($aNy*$routeExcess) (-$aNx) (-$aNy)}
elseif($dynamicB){Move-RopeDynamicObject $rope.EndB ($bNx*$routeExcess) ($bNy*$routeExcess) (-$bNx) (-$bNy)}
}
$mid=$points[[Math]::Floor($points.Count/2)]
if(-not$rope.Dragging){
$rope.X=$left+$mid.X-13;$rope.Y=$top+$mid.Y-13
if([double]::IsNaN($rope.LastHandleX)-or[Math]::Abs($rope.X-$rope.LastHandleX)-ge1-or[Math]::Abs($rope.Y-$rope.LastHandleY)-ge1){$rope.Window.Left=$rope.X;$rope.Window.Top=$rope.Y;$rope.LastHandleX=$rope.X;$rope.LastHandleY=$rope.Y}
}
$rope.LastVisualAX=$a.X;$rope.LastVisualAY=$a.Y;$rope.LastVisualBX=$b.X;$rope.LastVisualBY=$b.Y;$rope.LastVisualLength=$rope.Length;$rope.LastVisualTick=$script:ropeVisualTick
}
function Update-PlayroomRopes {
$script:ropeVisualTick++
$ropes=New-Object Collections.ArrayList
foreach($gadget in @($script:playroomGadgets)){if($gadget.Kind-eq'Rope'){$ropes.Add($gadget)|Out-Null}}
$script:ropeVisualDivisor=$(if($ropes.Count-ge6){3}elseif($ropes.Count-ge3){2}else{1})
$showShadow=$ropes.Count-lt5
$showFineHighlight=$ropes.Count-lt3
foreach($rope in $ropes){
if($rope.VisualPaths.Count-ge1-and$rope.ShadowVisible-ne$showShadow){$rope.VisualPaths[0].Visibility=$(if($showShadow){[Windows.Visibility]::Visible}else{[Windows.Visibility]::Collapsed});$rope.ShadowVisible=$showShadow}
if($rope.VisualPaths.Count-ge3-and$rope.FineHighlightVisible-ne$showFineHighlight){$rope.VisualPaths[2].Visibility=$(if($showFineHighlight){[Windows.Visibility]::Visible}else{[Windows.Visibility]::Collapsed});$rope.FineHighlightVisible=$showFineHighlight}
Update-OnePlayroomRope $rope
}
}
function Add-PlayroomBlock([string]$type) {
if (-not $script:playroomActive) { return }
$initialWidth=$(if($type-eq'Ladder'){56.0}else{132.0})
$initialHeight=$(if($type-eq'Ladder'){180.0}elseif($type-eq'Wooden'){18.0}else{30.0})
$blockWindow = New-Object Windows.Window
$blockWindow.Title = "Windowisp $type Block"
$blockWindow.Width = $initialWidth; $blockWindow.Height = $initialHeight
$blockWindow.WindowStyle = [Windows.WindowStyle]::None
$blockWindow.AllowsTransparency = $true; $blockWindow.Background = [Windows.Media.Brushes]::Transparent
$blockWindow.ShowInTaskbar = $false; $blockWindow.Topmost = $true
$blockWindow.ResizeMode = [Windows.ResizeMode]::NoResize
$block = New-Object Windows.Controls.Border
$block.MinWidth = $(if($type-eq'Ladder'){44}else{60}); $block.MinHeight = $(if($type-eq'Wooden'){8}else{20})
$block.Background = Get-BlockBrush $type
$block.ClipToBounds=$true
$block.BorderBrush = [Windows.Media.Brushes]::White; $block.BorderThickness = [Windows.Thickness]::new(2)
$block.CornerRadius = [Windows.CornerRadius]::new($(if ($type -eq 'Bouncy') { 12 } else { 4 }))
$block.Cursor = [Windows.Input.Cursors]::SizeAll
$label = New-Object Windows.Controls.TextBlock
$label.Text = $type.ToUpperInvariant(); $label.Foreground = [Windows.Media.Brushes]::White
$label.FontWeight = [Windows.FontWeights]::Bold; $label.FontSize = 11
$label.HorizontalAlignment = 'Center'; $label.VerticalAlignment = 'Center'
$blockContent=New-Object Windows.Controls.Grid
$blockContent.ClipToBounds=$true
$crumbleOverlay=New-Object Windows.Controls.Grid;$crumbleOverlay.IsHitTestVisible=$false;$crumbleOverlay.Opacity=0
$crumblePieces=New-Object Collections.ArrayList
$crumbleTranslate=New-Object Windows.Media.TranslateTransform
$crumbleScale=New-Object Windows.Media.ScaleTransform 1,1
$crumbleTransforms=New-Object Windows.Media.TransformGroup;$crumbleTransforms.Children.Add($crumbleScale);$crumbleTransforms.Children.Add($crumbleTranslate)
$blockContent.RenderTransformOrigin=[Windows.Point]::new(.5,1);$blockContent.RenderTransform=$crumbleTransforms
if($type-eq'Crumbling'){
$crackBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(220,70,38,24))
foreach($crackData in @('M10,0 L22,9 L17,17 L35,30','M58,0 L52,8 L67,15 L61,30','M99,0 L88,10 L103,19 L94,30','M124,2 L112,12 L121,22 L116,30')){
$crack=New-Object Windows.Shapes.Path;$crack.Data=[Windows.Media.Geometry]::Parse($crackData);$crack.Stroke=$crackBrush;$crack.StrokeThickness=2.2;$crack.Stretch='Fill';$crumbleOverlay.Children.Add($crack)|Out-Null
}
$crumbCanvas=New-Object Windows.Controls.Canvas;$crumbCanvas.ClipToBounds=$true
foreach($crumbSpec in @(@(12,5,5),@(30,17,4),@(51,8,6),@(76,19,4),@(96,4,5),@(116,14,6))){
$crumb=New-Object Windows.Shapes.Ellipse;$crumb.Width=$crumbSpec[2];$crumb.Height=[Math]::Max(3,$crumbSpec[2]-1)
$crumb.Fill=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(111,63,35));$crumb.Stroke=$crackBrush;$crumb.StrokeThickness=.8
[Windows.Controls.Canvas]::SetLeft($crumb,$crumbSpec[0]);[Windows.Controls.Canvas]::SetTop($crumb,$crumbSpec[1]);$crumbCanvas.Children.Add($crumb)|Out-Null;$crumblePieces.Add($crumb)|Out-Null
}
$crumbleOverlay.Children.Add($crumbCanvas)|Out-Null;$crumbleOverlay.Opacity=.12
}
$tint=New-Object Windows.Controls.Border
$tint.IsHitTestVisible=$false
$grassCap=$null;$mirrorShine=$null
if($type-eq'Grass'){
$grassCap=New-Object Windows.Controls.Border;$grassCap.Height=16;$grassCap.VerticalAlignment='Top';$grassCap.HorizontalAlignment='Stretch';$grassCap.Background=Get-GrassCapBrush;$grassCap.IsHitTestVisible=$false
}elseif($type-eq'Mirror'){
$mirrorShine=New-Object Windows.Controls.Border;$mirrorShine.HorizontalAlignment='Stretch';$mirrorShine.VerticalAlignment='Stretch';$mirrorShine.Opacity=.3;$mirrorShine.IsHitTestVisible=$false
$mirrorShine.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromArgb(0,255,255,255),[Windows.Media.Color]::FromArgb(190,205,247,255),0)
}
$conveyorArrows=$null;$conveyorBeltTransform=$null;$conveyorArrowTransform=$null
if($type-eq'Conveyor'){
$conveyorBeltTransform=New-Object Windows.Media.TranslateTransform
$block.Background.Transform=$conveyorBeltTransform
$conveyorArrows=New-Object Windows.Controls.TextBlock
$conveyorArrows.Text='> > > >';$conveyorArrows.FontFamily=New-Object Windows.Media.FontFamily('Segoe UI')
$conveyorArrows.FontSize=12;$conveyorArrows.FontWeight=[Windows.FontWeights]::Bold
$conveyorArrows.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(111,228,244))
$conveyorArrows.HorizontalAlignment='Center';$conveyorArrows.VerticalAlignment='Center'
$conveyorArrowTransform=New-Object Windows.Media.TranslateTransform;$conveyorArrows.RenderTransform=$conveyorArrowTransform
$conveyorArrows.IsHitTestVisible=$false;$conveyorArrows.Effect=New-Object Windows.Media.Effects.DropShadowEffect
$conveyorArrows.Effect.Color=[Windows.Media.Colors]::Black;$conveyorArrows.Effect.BlurRadius=3;$conveyorArrows.Effect.ShadowDepth=1
}
$label.IsHitTestVisible=$false
if($null-ne$grassCap){$blockContent.Children.Add($grassCap)|Out-Null}
if($null-ne$mirrorShine){$blockContent.Children.Add($mirrorShine)|Out-Null}
$blockContent.Children.Add($tint)|Out-Null
if($null-ne$conveyorArrows){$blockContent.Children.Add($conveyorArrows)|Out-Null}
if($type-eq'Crumbling'){$blockContent.Children.Add($crumbleOverlay)|Out-Null}
$blockContent.Children.Add($label)|Out-Null
$block.Child = $blockContent
$spawnX = [Windows.SystemParameters]::VirtualScreenLeft + (([Windows.SystemParameters]::VirtualScreenWidth - $initialWidth) / 2)
$spawnY = [Windows.SystemParameters]::VirtualScreenTop + (([Windows.SystemParameters]::VirtualScreenHeight - $initialHeight) / 2)
$handles = New-Object Collections.ArrayList
$blockData = [pscustomobject]@{ Kind='Block'; Window=$blockWindow; Element=$block; Label=$label; Tint=$tint; TintIndex=0; Handles=$handles; ContextMenu=$null; GrassCap=$grassCap; MirrorShine=$mirrorShine; ConveyorArrows=$conveyorArrows; ConveyorBeltTransform=$conveyorBeltTransform; ConveyorArrowTransform=$conveyorArrowTransform; CrumbleOverlay=$crumbleOverlay;CrumblePieces=$crumblePieces;CrumbleTranslate=$crumbleTranslate;CrumbleScale=$crumbleScale;CrumbleShakeTicks=0;ContactTicks=@{};VisualPhase=0.0; Direction=1; Type=$type; X=[double]$spawnX; Y=[double]$spawnY; Width=$initialWidth; Height=$initialHeight; Angle=0.0; TemporaryState='Stable';TemporaryTicks=0;Landings=0;WispfallGenerated=$false }
if($type-eq'Conveyor'){Set-ConveyorDirection $blockData 1}
$block.Tag = $blockData
$blockData.ContextMenu=New-BlockContextMenu $blockData
$block.ContextMenu=$blockData.ContextMenu
$blockWindow.Tag=$blockData
$blockWindow.Add_PreviewMouseRightButtonDown({
param($sender,$eventArgs)
if ($script:playroomActive) { $eventArgs.Handled=$true }
})
$blockWindow.Add_PreviewMouseRightButtonUp({
param($sender,$eventArgs)
if ($script:playroomActive -and $null -ne $sender.Tag.ContextMenu) {
$eventArgs.Handled=$true
$sender.Tag.ContextMenu.PlacementTarget=$sender.Tag.Element
$sender.Tag.ContextMenu.IsOpen=$true
}
})
$block.Add_MouseLeftButtonDown({
param($sender, $eventArgs)
$sender.Tag.Window.DragMove()
$sender.Tag.X = $sender.Tag.Window.Left+(($sender.Tag.Window.Width-$sender.Tag.Width)/2)
$sender.Tag.Y = $sender.Tag.Window.Top+(($sender.Tag.Window.Height-$sender.Tag.Height)/2)
Snap-PlacementToGrid $sender.Tag
Set-PlacementWindowPosition $sender.Tag
Rebase-FrozenRopeForPlacement $sender.Tag
$eventArgs.Handled = $true
})
$resizeGrid = New-Object Windows.Controls.Grid
$resizeGrid.Children.Add($block) | Out-Null
foreach ($direction in @('Left','Right','Top','Bottom','TopLeft','TopRight','BottomLeft','BottomRight')) {
if($type-in@('Wooden','Conveyor')-and$direction-match'Top|Bottom'){continue}
$thumb = New-Object Windows.Controls.Primitives.Thumb
$thumb.Tag = [pscustomobject]@{ Data=$blockData; Direction=$direction }
$thumb.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(145,255,255,255))
$thumb.Opacity = $(if ($direction -in @('Left','Right','Top','Bottom')) { 0.38 } else { 0.85 })
if ($direction -match 'Left') { $thumb.HorizontalAlignment='Left' }
elseif ($direction -match 'Right') { $thumb.HorizontalAlignment='Right' }
else { $thumb.HorizontalAlignment='Stretch' }
if ($direction -match 'Top') { $thumb.VerticalAlignment='Top' }
elseif ($direction -match 'Bottom') { $thumb.VerticalAlignment='Bottom' }
else { $thumb.VerticalAlignment='Stretch' }
if ($direction -in @('Left','Right')) { $thumb.Width=6; $thumb.Cursor=[Windows.Input.Cursors]::SizeWE }
elseif ($direction -in @('Top','Bottom')) { $thumb.Height=6; $thumb.Cursor=[Windows.Input.Cursors]::SizeNS }
else {
$thumb.Width=10; $thumb.Height=10
$thumb.Cursor=$(if($direction -in @('TopLeft','BottomRight')){[Windows.Input.Cursors]::SizeNWSE}else{[Windows.Input.Cursors]::SizeNESW})
}
$thumb.Add_DragDelta({
param($sender,$eventArgs)
$d=$sender.Tag.Data;$dir=$sender.Tag.Direction
$minimumHeight=$(if($d.Type-eq'Wooden'){8.0}else{20.0})
$newX=$d.X;$newY=$d.Y;$newW=$d.Width;$newH=$d.Height
$minimumWidth=$(if($d.Type-eq'Ladder'){44.0}else{60.0})
if($dir -match 'Right'){$newW=[Math]::Max($minimumWidth,$d.Width+$eventArgs.HorizontalChange)}
if($dir -match 'Bottom'){$newH=[Math]::Max($minimumHeight,$d.Height+$eventArgs.VerticalChange)}
if($dir -match 'Left'){$delta=[Math]::Min($eventArgs.HorizontalChange,$d.Width-$minimumWidth);$newX=$d.X+$delta;$newW=$d.Width-$delta}
if($dir -match 'Top'){$delta=[Math]::Min($eventArgs.VerticalChange,$d.Height-$minimumHeight);$newY=$d.Y+$delta;$newH=$d.Height-$delta}
$d.X=$newX;$d.Y=$newY;$d.Width=$newW;$d.Height=$newH
Set-PlacementAngle $d $d.Angle
})
$thumb.Add_DragCompleted({param($sender,$eventArgs);Snap-PlacementToGrid $sender.Tag.Data $true;Set-PlacementAngle $sender.Tag.Data $sender.Tag.Data.Angle})
$resizeGrid.Children.Add($thumb)|Out-Null
$handles.Add($thumb)|Out-Null
}
$blockWindow.Content = $resizeGrid
$blockWindow.Left = $spawnX; $blockWindow.Top = $spawnY
Register-RopeSelectable $blockWindow $blockData
$blockWindow.Show()
$script:playroomBlocks.Add($blockData) | Out-Null
}
function Register-SmoothToyDrag($surface,$data) {
if($null-eq$surface-or$null-eq$data){return}
$data|Add-Member -NotePropertyName DragOffsetX -NotePropertyValue 0.0 -Force
$data|Add-Member -NotePropertyName DragOffsetY -NotePropertyValue 0.0 -Force
$surface.Tag=$data
$surface.Add_MouseLeftButtonDown({
param($sender,$eventArgs)
if($script:wiringMode-or$script:ropeToolMode-or$script:eraserMode){return}
$cursor=New-Object PetNative+POINT
if(-not[PetNative]::GetCursorPos([ref]$cursor)){return}
$d=$sender.Tag;$d.CarriedBy='';$d.VX=0;$d.VY=0
if($d.Kind-eq'Bat'){$d.BatCharging=$false;$d.SwingTicks=0}
$d.DragOffsetX=$cursor.X-$d.X;$d.DragOffsetY=$cursor.Y-$d.Y;$d.Dragging=$true
$sender.CaptureMouse()|Out-Null;$eventArgs.Handled=$true
})
$surface.Add_MouseMove({
param($sender,$eventArgs)
$d=$sender.Tag
if(-not$d.Dragging-or$eventArgs.LeftButton-ne[Windows.Input.MouseButtonState]::Pressed){return}
$cursor=New-Object PetNative+POINT
if([PetNative]::GetCursorPos([ref]$cursor)){
$d.X=[double]$cursor.X-$d.DragOffsetX;$d.Y=[double]$cursor.Y-$d.DragOffsetY
Set-PlacementWindowPosition $d
}
$eventArgs.Handled=$true
})
$surface.Add_MouseLeftButtonUp({
param($sender,$eventArgs)
$d=$sender.Tag
if(-not$d.Dragging){return}
$d.Dragging=$false;$sender.ReleaseMouseCapture()
Snap-PlacementToGrid $d;Set-PlacementWindowPosition $d;Rebase-FrozenRopeForPlacement $d
$eventArgs.Handled=$true
})
$surface.Add_LostMouseCapture({
param($sender,$eventArgs)
if($null-ne$sender.Tag-and$sender.Tag.Dragging){$sender.Tag.Dragging=$false;Snap-PlacementToGrid $sender.Tag;Set-PlacementWindowPosition $sender.Tag;Rebase-FrozenRopeForPlacement $sender.Tag}
})
}
function Add-PlayroomBall {
if (-not $script:playroomActive) { return }
$size = 48.0
$ballWindow = New-Object Windows.Window
$ballWindow.Title = 'Windowisp Ball'
$ballWindow.Width = $size; $ballWindow.Height = $size
$ballWindow.WindowStyle = [Windows.WindowStyle]::None
$ballWindow.AllowsTransparency = $true; $ballWindow.Background = [Windows.Media.Brushes]::Transparent
$ballWindow.ShowInTaskbar = $false; $ballWindow.ShowActivated = $false
$ballWindow.Topmost = $true; $ballWindow.ResizeMode = [Windows.ResizeMode]::NoResize
$shape = New-Object Windows.Controls.Grid
$shape.Width = $size; $shape.Height = $size
# A transparent brush still participates in WPF hit testing. The artwork is
# deliberately non-hit-testable, so without this the visible ball cannot
# receive either drag or context-menu input.
$shape.Background=[Windows.Media.Brushes]::Transparent
$ballArt=New-ToyboxImage 'Ball'
if($null-ne$ballArt){$shape.Children.Add($ballArt)|Out-Null}
$shape.RenderTransformOrigin = [Windows.Point]::new(0.5,0.5)
$shape.RenderTransform = New-Object Windows.Media.RotateTransform 0
$spawnX = [Windows.SystemParameters]::VirtualScreenLeft + (([Windows.SystemParameters]::VirtualScreenWidth - $size) / 2)
$spawnY = [Windows.SystemParameters]::VirtualScreenTop + 100
$ball = [pscustomobject]@{ Kind='Ball'; Window=$ballWindow; Element=$shape; X=[double]$spawnX; Y=[double]$spawnY; VX=3.2; VY=0.0; Radius=24.0; Size=$size; Physics=[double]$script:ballPhysicsValue; Angle=0.0; PetHitCooldown=0; TeleportCooldown=0; CarriedBy=''; Dragging=$false }
Register-SmoothToyDrag $shape $ball
$shape.ContextMenu=New-PlacementContextMenu $ball $false
Register-RopeSelectable $ballWindow $ball
$ballWindow.Content = $shape; $ballWindow.Left = $spawnX; $ballWindow.Top = $spawnY
$ballWindow.Show()
$script:playroomBalls.Add($ball) | Out-Null
}
function Add-PlayroomToy([string]$kind) {
if(-not$script:playroomActive){return}
$size=54.0
$toyWindow=New-Object Windows.Window
$toyWindow.Title="Windowisp $kind";$toyWindow.Width=$size;$toyWindow.Height=$size
$toyWindow.WindowStyle=[Windows.WindowStyle]::None;$toyWindow.AllowsTransparency=$true
$toyWindow.Background=[Windows.Media.Brushes]::Transparent;$toyWindow.ShowInTaskbar=$false
$toyWindow.ShowActivated=$false;$toyWindow.Topmost=$true;$toyWindow.ResizeMode=[Windows.ResizeMode]::NoResize
$canvas=New-Object Windows.Controls.Grid;$canvas.Width=$size;$canvas.Height=$size
$canvas.Background=[Windows.Media.Brushes]::Transparent
$toyArt=New-ToyboxImage $kind
if($null-ne$toyArt){$canvas.Children.Add($toyArt)|Out-Null}
$canvas.RenderTransformOrigin=[Windows.Point]::new(.5,.5)
$visualRotate=New-Object Windows.Media.RotateTransform 0;$visualScale=$null
if($kind-eq'Bat'){
$visualScale=[Windows.Media.ScaleTransform]::new(1,1)
$visualTransforms=New-Object Windows.Media.TransformGroup
$visualTransforms.Children.Add($visualScale)|Out-Null;$visualTransforms.Children.Add($visualRotate)|Out-Null
$canvas.RenderTransform=$visualTransforms
}else{$canvas.RenderTransform=$visualRotate}
$spawnX=[Windows.SystemParameters]::VirtualScreenLeft+(([Windows.SystemParameters]::VirtualScreenWidth-$size)/2)
$spawnY=[Windows.SystemParameters]::VirtualScreenTop+90
$toy=[pscustomobject]@{Kind=$kind;Window=$toyWindow;Element=$canvas;VisualRotate=$visualRotate;VisualScale=$visualScale;SwingTicks=0;SwingDuration=18;SwingPower=0.0;SwingHitDone=$false;BatCharging=$false;BatChargeTicks=0;X=[double]$spawnX;Y=[double]$spawnY;VX=1.2;VY=0.0;Radius=27.0;Size=$size;Physics=38.0;Angle=$(if($kind-eq'Bat'){-28.0}else{0.0});PetHitCooldown=0;TeleportCooldown=0;CarriedBy='';Dragging=$false}
Register-SmoothToyDrag $canvas $toy
$canvas.ContextMenu=New-PlacementContextMenu $toy $false
Register-RopeSelectable $toyWindow $toy
Set-PlayroomObjectVisual $toy $toy.Angle
$toyWindow.Content=$canvas;$toyWindow.Left=$spawnX;$toyWindow.Top=$spawnY
$toyWindow.Show();$script:playroomBalls.Add($toy)|Out-Null
}
function Initialize-FanWind($fan) {
if($null-eq$fan-or$fan.Kind-ne'Fan'-or$fan.WindLines.Count-gt0){return}
Initialize-RopeOverlay
foreach($offset in @(-34.0,0.0,34.0)){
$line=New-Object Windows.Shapes.Polyline
$line.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(190,239,255))
$line.StrokeThickness=$(if($offset-eq0){2.4}else{1.7});$line.StrokeStartLineCap='Round';$line.StrokeEndLineCap='Round'
$line.StrokeDashArray=New-Object Windows.Media.DoubleCollection;$line.StrokeDashArray.Add(7);$line.StrokeDashArray.Add(6)
$line.Opacity=0;$line.IsHitTestVisible=$false
$script:ropeOverlayCanvas.Children.Add($line)|Out-Null;$fan.WindLines.Add([pscustomobject]@{Line=$line;Offset=$offset})|Out-Null
}
}
function Update-FanWind($fan,[bool]$powered) {
if($null-eq$fan-or$fan.Kind-ne'Fan'){return}
if($fan.WindLines.Count-eq0){Initialize-FanWind $fan}
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$r=$fan.Angle*[Math]::PI/180;$dx=[Math]::Cos($r);$dy=[Math]::Sin($r);$nx=-$dy;$ny=$dx
$originX=$fan.X+($fan.Width/2)+($dx*30);$originY=$fan.Y+($fan.Height/2)+($dy*30)
foreach($wind in @($fan.WindLines)){
$line=$wind.Line
if(-not$powered){$line.Opacity=0;continue}
$wave=[Math]::Sin(($script:inputTick*.15)+($wind.Offset*.08))*7
$points=New-Object Windows.Media.PointCollection
foreach($distance in @(12.0,105.0,215.0)){
$fadeOffset=$wind.Offset*($distance/215.0)
$points.Add([Windows.Point]::new($originX+($dx*$distance)+($nx*($fadeOffset+$wave))-$left,$originY+($dy*$distance)+($ny*($fadeOffset+$wave))-$top))
}
$line.Points=$points;$line.StrokeDashOffset=-1*($script:inputTick*.8);$line.Opacity=$(if($wind.Offset-eq0){.72}else{.48})
}
}
function Add-PlayroomGadget([string]$kind) {
if(-not$script:playroomActive){return}
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$width=[Windows.SystemParameters]::VirtualScreenWidth;$height=[Windows.SystemParameters]::VirtualScreenHeight
$kinds=$(if($kind-eq'Teleporter'){@('TeleporterA','TeleporterB')}else{@($kind)})
$index=0
foreach($gadgetKind in $kinds){
$w=$(if($gadgetKind-like'Teleporter*'){92.0}elseif($gadgetKind-eq'Fan'){76.0}elseif($gadgetKind-eq'Rotator'){40.0}elseif($gadgetKind-eq'PressurePlate'){94.0}elseif($gadgetKind-eq'Switch'){62.0}elseif($gadgetKind-in@('Button','Timer')){66.0}elseif($gadgetKind-eq'RopeCoil'){68.0}else{78.0})
$h=$(if($gadgetKind-like'Teleporter*'){72.0}elseif($gadgetKind-eq'Fan'){76.0}elseif($gadgetKind-eq'Rotator'){40.0}elseif($gadgetKind-eq'PressurePlate'){44.0}elseif($gadgetKind-eq'Switch'){84.0}elseif($gadgetKind-eq'Button'){58.0}elseif($gadgetKind-eq'Timer'){72.0}elseif($gadgetKind-eq'RopeCoil'){68.0}else{66.0})
$gadgetWindow=New-Object Windows.Window
$gadgetWindow.Title="Windowisp $gadgetKind";$gadgetWindow.Width=$w;$gadgetWindow.Height=$h
$gadgetWindow.WindowStyle=[Windows.WindowStyle]::None;$gadgetWindow.AllowsTransparency=$true
$gadgetWindow.Background=[Windows.Media.Brushes]::Transparent;$gadgetWindow.ShowInTaskbar=$false
$gadgetWindow.Topmost=$true;$gadgetWindow.ResizeMode=[Windows.ResizeMode]::NoResize
$border=New-Object Windows.Controls.Border;$border.CornerRadius=[Windows.CornerRadius]::new($(if($gadgetKind-eq'Rotator'){20}else{9}))
$colour=$(if($gadgetKind-eq'Spring'){[Windows.Media.Color]::FromRgb(72,194,104)}elseif($gadgetKind-like'Teleporter*'){[Windows.Media.Color]::FromRgb($(if($gadgetKind-eq'TeleporterA'){74}else{210}),91,$(if($gadgetKind-eq'TeleporterA'){220}else{157}))}elseif($gadgetKind-eq'Rotator'){[Windows.Media.Color]::FromRgb(179,91,215)}else{[Windows.Media.Color]::FromRgb(55,155,205)})
$border.Background=[Windows.Media.Brushes]::Transparent
$border.BorderThickness=[Windows.Thickness]::new(0)
$label=New-Object Windows.Controls.TextBlock
$label.Text=$(if($gadgetKind-eq'Spring'){'^^ SPRING'}elseif($gadgetKind-like'Teleporter*'){'O PORTAL'}elseif($gadgetKind-eq'Rotator'){[string][char]0x21BB}elseif($gadgetKind-eq'RopeCoil'){'ROPE'}elseif($gadgetKind-in@('Switch','Button','PressurePlate','Timer')){"$($gadgetKind.ToUpperInvariant()) OFF"}else{'FAN 0 deg'})
$label.FontFamily=New-Object Windows.Media.FontFamily('Segoe UI Emoji');$label.FontSize=$(if($gadgetKind-eq'Fan'){13}else{11})
$label.FontWeight=[Windows.FontWeights]::Bold;$label.Foreground=[Windows.Media.Brushes]::White
$label.HorizontalAlignment='Center';$label.VerticalAlignment='Bottom';$label.Visibility=[Windows.Visibility]::Collapsed
if($gadgetKind-in@('Switch','Button','PressurePlate','Timer')){$label.Visibility=[Windows.Visibility]::Visible;$label.FontSize=9;$label.Margin=[Windows.Thickness]::new(2)}
$gadgetContent=New-Object Windows.Controls.Grid
$gadgetArt=New-ToyboxImage $(if($gadgetKind-like'Teleporter*'){'Teleporter'}else{$gadgetKind})
if($gadgetKind-in@('Switch','Button','PressurePlate','Timer')-and$null-eq$gadgetArt){
$gadgetArt=New-Object Windows.Controls.Border;$gadgetArt.CornerRadius=[Windows.CornerRadius]::new($(if($gadgetKind-eq'PressurePlate'){5}else{12}))
$logicColours=@{Switch=[Windows.Media.Color]::FromRgb(70,134,205);Button=[Windows.Media.Color]::FromRgb(218,74,91);PressurePlate=[Windows.Media.Color]::FromRgb(222,170,54);Timer=[Windows.Media.Color]::FromRgb(145,87,208)}
$gadgetArt.Background=New-Object Windows.Media.SolidColorBrush $logicColours[$gadgetKind];$gadgetArt.BorderBrush=[Windows.Media.Brushes]::White;$gadgetArt.BorderThickness=[Windows.Thickness]::new(2)
}
$springScale=$null;$springBase=$null
if($gadgetKind-eq'Spring'-and$null-ne$gadgetArt){
$springBase=New-Object Windows.Controls.Border;$springBase.Width=$w*.82;$springBase.Height=10;$springBase.VerticalAlignment='Bottom';$springBase.HorizontalAlignment='Center'
$springBase.CornerRadius=[Windows.CornerRadius]::new(5);$springBase.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(20,48,75),[Windows.Media.Color]::FromRgb(7,18,35),90)
$springBase.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(73,211,235));$springBase.BorderThickness=[Windows.Thickness]::new(1.3);$springBase.IsHitTestVisible=$false
$springScale=[Windows.Media.ScaleTransform]::new(1,1)
$gadgetArt.RenderTransformOrigin=[Windows.Point]::new(.5,1)
$gadgetArt.RenderTransform=$springScale
}
$logicScale=$null;$logicRotate=$null;$logicTranslate=$null;$timerDial=$null;$timerDialTransform=$null
if($gadgetKind-in@('Switch','Button','PressurePlate','Timer')-and$null-ne$gadgetArt){
$logicScale=[Windows.Media.ScaleTransform]::new(1,1);$logicRotate=[Windows.Media.RotateTransform]::new(0);$logicTranslate=[Windows.Media.TranslateTransform]::new(0,0)
$logicGroup=New-Object Windows.Media.TransformGroup;$logicGroup.Children.Add($logicScale)|Out-Null;$logicGroup.Children.Add($logicRotate)|Out-Null;$logicGroup.Children.Add($logicTranslate)|Out-Null
$gadgetArt.RenderTransformOrigin=[Windows.Point]::new(.5,1);$gadgetArt.RenderTransform=$logicGroup
}
if($null-ne$springBase){$gadgetContent.Children.Add($springBase)|Out-Null}
if($null-ne$gadgetArt){$gadgetContent.Children.Add($gadgetArt)|Out-Null}
$gadgetContent.Children.Add($label)|Out-Null
$border.Child=$gadgetContent
$spawnX=$(if($gadgetKind-eq'TeleporterA'){$left+100}elseif($gadgetKind-eq'TeleporterB'){$left+$width-$w-100}else{$left+(($width-$w)/2)+($index*100)})
$spawnY=$top+$height-$h-80
$gadget=[pscustomobject]@{Kind=$gadgetKind;Window=$gadgetWindow;Element=$border;Label=$label;Art=$gadgetArt;WindLines=(New-Object Collections.ArrayList);SpringScale=$springScale;SpringBase=$springBase;CompressionTicks=0;LogicScale=$logicScale;LogicRotate=$logicRotate;LogicTranslate=$logicTranslate;TimerDial=$timerDial;TimerDialTransform=$timerDialTransform;PreviousSignalOn=$false;StateAnimTicks=0;SignalOn=$false;PulseTicks=0;ManualPower=$(if($gadgetKind-in@('Fan','Rotator','TeleporterA','TeleporterB')){$true}else{$null});X=[double]$spawnX;Y=[double]$spawnY;Width=$w;Height=$h;BodyWidth=$w;BodyHeight=$h;BodyOffsetX=0.0;BodyOffsetY=0.0;VX=0.0;VY=0.0;Dragging=$false;IsDynamic=($gadgetKind-in@('Fan','TeleporterA','TeleporterB'));SpringCooldown=0;PhysicsStartY=[double]$spawnY;PhysicsDebugSeen=$false;Direction=1;RotationSpeed=1.0;Angle=0.0;SpinAngle=0.0;SpinTransform=$null;Target=$null;DebugSeen=$false;GameMarker=$false}
$border.Tag=$gadget
if($gadgetKind-eq'Rotator'){
$gadget.SpinTransform=New-Object Windows.Media.RotateTransform 0
$border.RenderTransformOrigin=[Windows.Point]::new(.5,.5);$border.RenderTransform=$gadget.SpinTransform
$label.FontSize=22
}
$border.Add_MouseLeftButtonDown({
param($sender,$e)
if($script:wiringMode-or$script:ropeToolMode-or$script:eraserMode){return}
$data=$sender.Tag;$beforeX=$data.Window.Left;$beforeY=$data.Window.Top
if($null-ne$data.PSObject.Properties['Dragging']){$data.VX=0;$data.VY=0;$data.Dragging=$true}
try{$data.Window.DragMove()}finally{if($null-ne$data.PSObject.Properties['Dragging']){$data.Dragging=$false}}
$moved=[Math]::Abs($data.Window.Left-$beforeX)+[Math]::Abs($data.Window.Top-$beforeY)
if($moved-ge4){Rebase-FrozenRopeForPlacement $data}
if($moved-lt4){if($data.Kind-eq'Switch'){$data.SignalOn=-not$data.SignalOn}elseif($data.Kind-eq'Button'){$data.PulseTicks=90;$data.SignalOn=$true}}
})
$border.ContextMenu=New-PlacementContextMenu $gadget ($gadgetKind-ne'Rotator')
Register-RopeSelectable $gadgetWindow $gadget
$gadgetWindow.Add_LocationChanged({param($sender,$e);$data=@($script:playroomGadgets|Where-Object{$_.Window-eq$sender}|Select-Object -First 1);if($data.Count){$data[0].X=$sender.Left+(($sender.Width-$data[0].Width)/2);$data[0].Y=$sender.Top+(($sender.Height-$data[0].Height)/2)}})
$gadgetWindow.Content=$border;$gadgetWindow.Left=$spawnX;$gadgetWindow.Top=$spawnY;$gadgetWindow.Show()
if($gadgetKind-eq'Fan'){Initialize-FanWind $gadget}
$script:playroomGadgets.Add($gadget)|Out-Null;$index++
}
}
function Add-PlayroomHazard([string]$kind) {
if(-not$script:playroomActive){return}
$screenLeft=[Windows.SystemParameters]::VirtualScreenLeft;$screenTop=[Windows.SystemParameters]::VirtualScreenTop
$screenWidth=[Windows.SystemParameters]::VirtualScreenWidth;$screenHeight=[Windows.SystemParameters]::VirtualScreenHeight
$w=$(if($kind-eq'Laser'){300.0}elseif($kind-eq'FireJet'){54.0}else{106.0})
$h=$(if($kind-eq'Laser'){300.0}elseif($kind-eq'FireJet'){116.0}else{30.0})
$hazardWindow=New-Object Windows.Window
$hazardWindow.Title="Windowisp $kind";$hazardWindow.Width=$w;$hazardWindow.Height=$h
$hazardWindow.WindowStyle=[Windows.WindowStyle]::None;$hazardWindow.AllowsTransparency=$true
$hazardWindow.Background=[Windows.Media.Brushes]::Transparent;$hazardWindow.ShowInTaskbar=$false
$hazardWindow.Topmost=$true;$hazardWindow.ResizeMode=[Windows.ResizeMode]::NoResize
$canvas=New-Object Windows.Controls.Canvas;$canvas.Width=$w;$canvas.Height=$h
$visual=$null;$emitter=$null;$reflection=$null;$beamWindow=$null;$beamLines=$null;$beamGlowLines=$null;$fireTransform=$null
if($kind-eq'Spikes'){
$visual=New-ToyboxImage 'Spikes';$visual.Width=$w;$visual.Height=$h
$visual.Stretch=[Windows.Media.Stretch]::Fill
$canvas.Children.Add($visual)|Out-Null
$emitter=New-Object Windows.Controls.Border;$emitter.Width=$w;$emitter.Height=$h
$emitter.Background=[Windows.Media.Brushes]::Transparent
$canvas.Children.Add($emitter)|Out-Null
}elseif($kind-eq'FireJet'){
$visual=New-Object Windows.Controls.Canvas;$visual.Width=54;$visual.Height=76
$outerFlame=New-Object Windows.Shapes.Path
$outerFlame.Data=[Windows.Media.Geometry]::Parse('M27,76 C10,72 4,61 10,48 C15,38 12,29 19,15 C21,28 28,28 31,3 C43,22 39,34 47,45 C57,61 45,73 27,76 Z')
$outerFlame.Fill=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(255,194,28),[Windows.Media.Color]::FromRgb(219,35,25),90)
$outerFlame.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(210,104,19,48));$outerFlame.StrokeThickness=1.5
$outerFlame.Effect=New-Object Windows.Media.Effects.DropShadowEffect
$outerFlame.Effect.Color=[Windows.Media.Color]::FromRgb(255,69,20);$outerFlame.Effect.BlurRadius=15;$outerFlame.Effect.ShadowDepth=0
$visual.Children.Add($outerFlame)|Out-Null
$middleFlame=New-Object Windows.Shapes.Path
$middleFlame.Data=[Windows.Media.Geometry]::Parse('M27,71 C17,68 14,59 19,50 C23,43 21,36 27,25 C30,39 38,42 39,52 C42,62 36,69 27,71 Z')
$middleFlame.Fill=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(255,249,128),[Windows.Media.Color]::FromRgb(255,111,18),90)
$visual.Children.Add($middleFlame)|Out-Null
$coreFlame=New-Object Windows.Shapes.Path
$coreFlame.Data=[Windows.Media.Geometry]::Parse('M27,69 C21,65 22,58 27,49 C34,58 34,65 27,69 Z')
$coreFlame.Fill=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,255,224))
$visual.Children.Add($coreFlame)|Out-Null
$fireTransform=[Windows.Media.ScaleTransform]::new(1,1)
$visual.RenderTransformOrigin=[Windows.Point]::new(.5,1);$visual.RenderTransform=$fireTransform
[Windows.Controls.Canvas]::SetLeft($visual,0);[Windows.Controls.Canvas]::SetTop($visual,-7)
$canvas.Children.Add($visual)|Out-Null
$emitter=New-Object Windows.Controls.Border;$emitter.Width=54;$emitter.Height=54
$fireSource=Get-ToyboxImageSource 'FireJet'
$emitter.Background=$(if($null-ne$fireSource){New-Object Windows.Media.ImageBrush $fireSource}else{New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(76,67,74))})
[Windows.Controls.Canvas]::SetTop($emitter,62);$canvas.Children.Add($emitter)|Out-Null
}else{
$beamWindow=New-Object Windows.Window
$beamWindow.Title='Windowisp Laser Beam'
$beamWindow.Width=$screenWidth;$beamWindow.Height=$screenHeight
$beamWindow.Left=$screenLeft;$beamWindow.Top=$screenTop
$beamWindow.WindowStyle=[Windows.WindowStyle]::None;$beamWindow.AllowsTransparency=$true
$beamWindow.Background=[Windows.Media.Brushes]::Transparent;$beamWindow.ShowInTaskbar=$false
$beamWindow.ShowActivated=$false;$beamWindow.Topmost=$true;$beamWindow.ResizeMode=[Windows.ResizeMode]::NoResize
$beamCanvas=New-Object Windows.Controls.Canvas
$beamCanvas.Width=$screenWidth;$beamCanvas.Height=$screenHeight;$beamCanvas.IsHitTestVisible=$false
$beamLines=New-Object Collections.ArrayList;$beamGlowLines=New-Object Collections.ArrayList
foreach($segmentIndex in 0..7){
$glow=New-Object Windows.Shapes.Line
$glow.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(115,255,18,46))
$glow.StrokeThickness=13;$glow.Visibility=[Windows.Visibility]::Hidden
$glow.Effect=New-Object Windows.Media.Effects.DropShadowEffect
$glow.Effect.Color=[Windows.Media.Color]::FromRgb(255,0,36);$glow.Effect.BlurRadius=24;$glow.Effect.ShadowDepth=0
$beamCanvas.Children.Add($glow)|Out-Null;$beamGlowLines.Add($glow)|Out-Null
$line=New-Object Windows.Shapes.Line
$line.Stroke=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(255,255,255),[Windows.Media.Color]::FromRgb(255,72,92),0)
$line.StrokeThickness=3.2;$line.Visibility=[Windows.Visibility]::Hidden
$line.Effect=New-Object Windows.Media.Effects.DropShadowEffect
$line.Effect.Color=[Windows.Media.Color]::FromRgb(255,52,70);$line.Effect.BlurRadius=7;$line.Effect.ShadowDepth=0
$beamCanvas.Children.Add($line)|Out-Null;$beamLines.Add($line)|Out-Null
}
$beamWindow.Content=$beamCanvas
$visual=$beamLines[0];$reflection=$beamLines[1]
$emitter=New-Object Windows.Controls.Border;$emitter.Width=62;$emitter.Height=62;$emitter.CornerRadius=[Windows.CornerRadius]::new(31)
$laserSource=Get-ToyboxImageSource 'Laser'
$emitter.Background=$(if($null-ne$laserSource){New-Object Windows.Media.ImageBrush $laserSource}else{New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(92,25,38))})
[Windows.Controls.Canvas]::SetLeft($emitter,119);[Windows.Controls.Canvas]::SetTop($emitter,119);$canvas.Children.Add($emitter)|Out-Null
}
$spawnX=$screenLeft+(($screenWidth-$w)/2);$spawnY=$screenTop+$screenHeight-$h-95
$bodyWidth=$(if($kind-eq'Laser'){62.0}elseif($kind-eq'FireJet'){54.0}else{$w})
$bodyHeight=$(if($kind-eq'Laser'){62.0}elseif($kind-eq'FireJet'){54.0}else{$h})
$bodyOffsetX=$(if($kind-eq'Laser'){119.0}else{0.0})
$bodyOffsetY=$(if($kind-eq'Laser'){119.0}elseif($kind-eq'FireJet'){62.0}else{0.0})
$hazard=[pscustomobject]@{Kind=$kind;Window=$hazardWindow;Element=$canvas;Visual=$visual;Reflection=$reflection;ReflectedSegment=$null;BeamWindow=$beamWindow;BeamLines=$beamLines;BeamGlowLines=$beamGlowLines;BeamSegments=(New-Object Collections.ArrayList);FireTransform=$fireTransform;Emitter=$emitter;ManualPower=$true;X=[double]$spawnX;Y=[double]$spawnY;Width=$w;Height=$h;BodyWidth=$bodyWidth;BodyHeight=$bodyHeight;BodyOffsetX=$bodyOffsetX;BodyOffsetY=$bodyOffsetY;VX=0.0;VY=0.0;Dragging=$false;IsDynamic=($kind-in@('Spikes','Laser','FireJet'));SpringCooldown=0;PhysicsStartY=[double]$spawnY;PhysicsDebugSeen=$false;Angle=0.0;Active=$true}
$emitter.Tag=$hazard
$emitter.Add_MouseLeftButtonDown({
param($sender,$e)
$data=$sender.Tag;$data.VX=0;$data.VY=0;$data.Dragging=$true
try{$data.Window.DragMove()}finally{$data.Dragging=$false}
# Synchronise logical coordinates once after a real user drag. Do not use
# LocationChanged here: programmatic Left/Top updates arrive separately,
# and that callback was restoring the previous Y during every physics tick.
$data.X=$data.Window.Left+(($data.Window.Width-$data.Width)/2)
$data.Y=$data.Window.Top+(($data.Window.Height-$data.Height)/2)
$data.VX=0;$data.VY=0
Rebase-FrozenRopeForPlacement $data
})
$emitter.ContextMenu=New-PlacementContextMenu $hazard $true
Register-RopeSelectable $hazardWindow $hazard
$hazardWindow.Add_LocationChanged({
param($sender,$e)
$data=$sender.Tag
if($null-ne$data-and$data.Dragging){
$data.X=$sender.Left+(($sender.Width-$data.Width)/2)
$data.Y=$sender.Top+(($sender.Height-$data.Height)/2)
}
})
$hazardWindow.Content=$canvas;$hazardWindow.Left=$spawnX;$hazardWindow.Top=$spawnY;$hazardWindow.Show()
if($null-ne$beamWindow){$beamWindow.Show();Set-OverlayClickThrough $beamWindow}
$script:playroomGadgets.Add($hazard)|Out-Null
}
function Invoke-SandboxHazardDamage([string]$player) {
if($player-eq'P2'){
if(-not$script:twoPlayerActive-or$script:p2Hearts-le0-or$script:p2InvulnerableTicks-gt0){return}
if($script:playroomActive){$script:p2Hearts=[Math]::Max(1,$script:p2Hearts-1)}
else{$script:p2Hearts--}
$script:p2InvulnerableTicks=90
}else{
if($script:hearts-le0-or$script:invulnerableTicks-gt0){return}
if($script:playroomActive){$script:hearts=[Math]::Max(1,$script:hearts-1)}
else{$script:hearts--}
$script:invulnerableTicks=90
if(-not$script:playroomActive-and$script:hearts-le0){$script:active=$false;Register-ControlKeys $false;Set-ClickThrough $true;Update-Menu}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:sandbox-damage player=$player hearts=$($script:hearts) p2Hearts=$($script:p2Hearts) active=$($script:active) p1Ticks=$($script:invulnerableTicks) p2Ticks=$($script:p2InvulnerableTicks)")}
}
function Test-PointNearSegment([double]$px,[double]$py,[double]$x1,[double]$y1,[double]$x2,[double]$y2,[double]$radius) {
$dx=$x2-$x1;$dy=$y2-$y1;$lengthSquared=($dx*$dx)+($dy*$dy)
if($lengthSquared-le0){return $false}
$t=[Math]::Max(0.0,[Math]::Min(1.0,(($px-$x1)*$dx+($py-$y1)*$dy)/$lengthSquared))
$cx=$x1+($t*$dx);$cy=$y1+($t*$dy)
return ((($px-$cx)*($px-$cx)+($py-$cy)*($py-$cy))-le($radius*$radius))
}
function Test-RotatedPlacementPoint($data,[double]$px,[double]$py,[double]$padding=0) {
$cx=$data.X+$data.Width/2;$cy=$data.Y+$data.Height/2
$r=-1*$data.Angle*[Math]::PI/180;$dx=$px-$cx;$dy=$py-$cy
$localX=($dx*[Math]::Cos($r))-($dy*[Math]::Sin($r))
$localY=($dx*[Math]::Sin($r))+($dy*[Math]::Cos($r))
return ([Math]::Abs($localX)-le$data.Width/2+$padding-and[Math]::Abs($localY)-le$data.Height/2+$padding)
}
function Get-RayDesktopDistance([double]$ox,[double]$oy,[double]$dx,[double]$dy) {
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$right=$left+[Windows.SystemParameters]::VirtualScreenWidth;$bottom=$top+[Windows.SystemParameters]::VirtualScreenHeight
$distances=New-Object Collections.ArrayList
if($dx-gt.000001){$distances.Add(($right-$ox)/$dx)|Out-Null}elseif($dx-lt-.000001){$distances.Add(($left-$ox)/$dx)|Out-Null}
if($dy-gt.000001){$distances.Add(($bottom-$oy)/$dy)|Out-Null}elseif($dy-lt-.000001){$distances.Add(($top-$oy)/$dy)|Out-Null}
return [double](@($distances|Where-Object{$_-gt.01}|Measure-Object -Minimum).Minimum)
}
function Get-RayPlacementHit($data,[double]$ox,[double]$oy,[double]$dx,[double]$dy,[double]$maxDistance) {
$angle=$(if($null-ne$data.PSObject.Properties['Angle']){[double]$data.Angle}else{0.0})
$r=$angle*[Math]::PI/180;$ux=[Math]::Cos($r);$uy=[Math]::Sin($r);$vx=-[Math]::Sin($r);$vy=[Math]::Cos($r)
$width=[double]$data.Width;$height=[double]$data.Height
$cx=[double]$data.X+($width/2.0);$cy=[double]$data.Y+($height/2.0);$rx=$ox-$cx;$ry=$oy-$cy
$localOx=($rx*$ux)+($ry*$uy);$localOy=($rx*$vx)+($ry*$vy)
$localDx=($dx*$ux)+($dy*$uy);$localDy=($dx*$vx)+($dy*$vy)
$tNear=.5;$tFar=$maxDistance;$normalLocalX=0.0;$normalLocalY=0.0
foreach($axis in @(@($localOx,$localDx,($width/2.0),1),@($localOy,$localDy,($height/2.0),2))){
$origin=[double]$axis[0];$direction=[double]$axis[1];$half=[double]$axis[2]
if([Math]::Abs($direction)-lt.000001){if([Math]::Abs($origin)-gt$half){return $null};continue}
$t1=(-$half-$origin)/$direction;$t2=($half-$origin)/$direction
$near=[Math]::Min($t1,$t2);$far=[Math]::Max($t1,$t2)
if($near-gt$tNear){
$tNear=$near
if($axis[3]-eq1){$normalLocalX=$(if($t1-lt$t2){-1.0}else{1.0});$normalLocalY=0.0}
else{$normalLocalX=0.0;$normalLocalY=$(if($t1-lt$t2){-1.0}else{1.0})}
}
$tFar=[Math]::Min($tFar,$far)
if($tNear-gt$tFar){return $null}
}
if($tNear-lt.5-or$tNear-gt$maxDistance){return $null}
return [pscustomobject]@{Distance=$tNear;NormalX=($normalLocalX*$ux)+($normalLocalY*$vx);NormalY=($normalLocalX*$uy)+($normalLocalY*$vy);Object=$data}
}
function Get-RayCircleHit($data,[double]$ox,[double]$oy,[double]$dx,[double]$dy,[double]$maxDistance) {
$size=[double]$data.Size;$radius=$size/2.0
$cx=[double]$data.X+$radius;$cy=[double]$data.Y+$radius;$rx=$ox-$cx;$ry=$oy-$cy
$b=($rx*$dx)+($ry*$dy);$c=($rx*$rx)+($ry*$ry)-($radius*$radius)
$disc=($b*$b)-$c;if($disc-lt0){return $null}
$distance=-$b-[Math]::Sqrt($disc);if($distance-lt.5-or$distance-gt$maxDistance){return $null}
$hx=$ox+$dx*$distance;$hy=$oy+$dy*$distance;$nx=($hx-$cx)/$radius;$ny=($hy-$cy)/$radius
return [pscustomobject]@{Distance=$distance;NormalX=$nx;NormalY=$ny;Object=$data}
}
function Update-LaserReflection($laser) {
if($null-eq$laser.BeamLines){return}
foreach($line in @($laser.BeamLines)){$line.Visibility=[Windows.Visibility]::Hidden}
foreach($line in @($laser.BeamGlowLines)){$line.Visibility=[Windows.Visibility]::Hidden}
$laser.BeamSegments.Clear();$laser.ReflectedSegment=$null
$screenLeft=[Windows.SystemParameters]::VirtualScreenLeft;$screenTop=[Windows.SystemParameters]::VirtualScreenTop
$r=$laser.Angle*[Math]::PI/180;$dx=[Math]::Cos($r);$dy=[Math]::Sin($r)
$centreX=$laser.X+150;$centreY=$laser.Y+150
$ox=$centreX+($dx*29);$oy=$centreY+($dy*29)
for($bounce=0;$bounce-lt$laser.BeamLines.Count;$bounce++){
$maxDistance=Get-RayDesktopDistance $ox $oy $dx $dy
$nearest=$null
foreach($candidate in @($script:playroomBlocks)){
if($candidate.Type-eq'Ladder'){continue}
$hit=Get-RayPlacementHit $candidate $ox $oy $dx $dy $maxDistance
if($null-ne$hit-and($null-eq$nearest-or$hit.Distance-lt$nearest.Distance)){$nearest=$hit}
}
foreach($candidate in @($script:playroomBalls)){
$hit=Get-RayCircleHit $candidate $ox $oy $dx $dy $maxDistance
if($null-ne$hit-and($null-eq$nearest-or$hit.Distance-lt$nearest.Distance)){$nearest=$hit}
}
foreach($candidate in @($script:playroomGadgets|Where-Object{$_-ne$laser-and$_.Kind-notin@('Rotator','Rope')-and$_.Window.IsVisible})){
$hit=Get-RayPlacementHit $candidate $ox $oy $dx $dy $maxDistance
if($null-ne$hit-and($null-eq$nearest-or$hit.Distance-lt$nearest.Distance)){$nearest=$hit}
}
$distance=$(if($null-ne$nearest){$nearest.Distance}else{$maxDistance})
$endX=$ox+$dx*$distance;$endY=$oy+$dy*$distance
$segment=[pscustomobject]@{X1=$ox;Y1=$oy;X2=$endX;Y2=$endY}
$laser.BeamSegments.Add($segment)|Out-Null
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_LASER_VISUAL-eq'1'-and$bounce-eq0-and-not$script:diagnosticLaserSegmentMarker){
[Console]::Out.WriteLine("diagnostic:laser-segment x1=$($segment.X1) y1=$($segment.Y1) x2=$($segment.X2) y2=$($segment.Y2) playerX=$($script:x+$PetWidth/2) playerY=$($script:y+$PetHeight/2)")
$script:diagnosticLaserSegmentMarker=$true
}
$line=$laser.BeamLines[$bounce]
$line.X1=$ox-$screenLeft;$line.Y1=$oy-$screenTop;$line.X2=$endX-$screenLeft;$line.Y2=$endY-$screenTop
$line.Visibility=[Windows.Visibility]::Visible
$glow=$laser.BeamGlowLines[$bounce]
$glow.X1=$line.X1;$glow.Y1=$line.Y1;$glow.X2=$line.X2;$glow.Y2=$line.Y2
$glow.Opacity=.82+(.12*[Math]::Sin(($script:hazardTick+$bounce*9)*.24))
$glow.Visibility=[Windows.Visibility]::Visible
if($bounce-eq1){$laser.ReflectedSegment=$segment}
$isMirror=$null-ne$nearest-and$nearest.Object.Kind-eq'Block'-and$nearest.Object.Type-eq'Mirror'
if(-not$isMirror){break}
$dot=($dx*$nearest.NormalX)+($dy*$nearest.NormalY)
$dx=$dx-(2*$dot*$nearest.NormalX);$dy=$dy-(2*$dot*$nearest.NormalY)
$ox=$endX+($dx*.75);$oy=$endY+($dy*.75)
}
}
function Update-PlayroomHazards {
if(-not$script:playroomActive-and-not$script:active){return}
$script:hazardTick++
if($env:WINDOWISP_TEST_CRUMBLE_ANIMATION-eq'1'-and$null-ne$script:crumbleTestBlock){
$testAge=$script:hazardTick-$script:crumbleTestStartTick
if(($testAge-ge15-and$testAge-le35)-or($testAge-ge65-and$testAge-le85)-or($testAge-ge115-and$testAge-le135)){
$before=$script:crumbleTestBlock.Landings;Trigger-TemporaryBlock $script:crumbleTestBlock 'DiagnosticWeight'
if($script:crumbleTestBlock.Landings-ne$before){[Console]::Out.WriteLine("diagnostic:crumble-impact count=$($script:crumbleTestBlock.Landings) tick=$testAge")}
}
}
foreach($h in @($script:playroomGadgets|Where-Object{$_.Kind-in@('Spikes','FireJet','Laser')})){
$wiredOn=Test-HazardWiredOn $h
if($null-ne$h.Emitter){$h.Emitter.Opacity=$(if($wiredOn){1}else{.42})}
if($null-ne$h.Element){$h.Element.Opacity=$(if($wiredOn){1}else{.42})}
$updateHazardArtwork=$script:playroomActive-or(($script:hazardTick%2)-eq0)
if($h.Kind-eq'FireJet'){
# A powered Fire Blaster stays on. Timing/pulsing belongs to the Timer
# gadget, so an unwired or manually enabled blaster must not flicker.
$h.Active=$wiredOn
$h.Visual.Visibility=$(if($h.Active){[Windows.Visibility]::Visible}else{[Windows.Visibility]::Hidden})
if($h.Active-and$null-ne$h.FireTransform-and$updateHazardArtwork){
$h.FireTransform.ScaleX=.94+(.08*[Math]::Sin($script:hazardTick*.71))
$h.FireTransform.ScaleY=.91+(.12*[Math]::Sin(($script:hazardTick*.93)+.8))
$h.Visual.Opacity=.9+(.1*[Math]::Sin($script:hazardTick*.47))
}
}
if($h.Kind-eq'Laser'){
if($wiredOn){if($updateHazardArtwork-or$h.BeamSegments.Count-eq0){Update-LaserReflection $h}}else{$h.BeamSegments.Clear();foreach($line in @($h.BeamLines)+@($h.BeamGlowLines)){$line.Visibility=[Windows.Visibility]::Hidden}}
}
foreach($player in @('P1','P2')){
if($player-eq'P2'-and-not$script:twoPlayerActive){continue}
$px=$(if($player-eq'P2'){$script:p2X}else{$script:x});$py=$(if($player-eq'P2'){$script:p2Y}else{$script:y})
$hit=$false
if($h.Kind-eq'Spikes'-and$wiredOn){
$hit=Test-RotatedPlacementPoint $h ($px+$PetWidth/2) ($py+$PetHeight-8) 13
}elseif($h.Kind-eq'FireJet'-and$h.Active){
$hit=Test-RotatedPlacementPoint $h ($px+$PetWidth/2) ($py+$PetHeight/2) 22
}elseif($h.Kind-eq'Laser'-and$wiredOn){
for($segmentIndex=0;$segmentIndex-lt$h.BeamSegments.Count;$segmentIndex++){
$segment=$h.BeamSegments[$segmentIndex]
$segmentHit=Test-PointNearSegment ($px+$PetWidth/2) ($py+$PetHeight/2) $segment.X1 $segment.Y1 $segment.X2 $segment.Y2 28
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_LASER_VISUAL-eq'1'-and$player-eq'P1'-and$segmentIndex-eq0-and-not$script:diagnosticLaserHitMarker){
[Console]::Out.WriteLine("diagnostic:laser-hit-check hit=$segmentHit hearts=$($script:hearts) ticks=$($script:invulnerableTicks)")
$script:diagnosticLaserHitMarker=$true
}
if($segmentHit){$hit=$true;break}
}
}
if($hit){Invoke-SandboxHazardDamage $player}
}
}
}
function Get-SpringTopContact($spring,[double]$centreX,[double]$centreY,[double]$radius,[double]$velocityX,[double]$velocityY) {
$r=$spring.Angle*[Math]::PI/180
$ux=[Math]::Cos($r);$uy=[Math]::Sin($r);$vx=-[Math]::Sin($r);$vy=[Math]::Cos($r)
$springCentreX=$spring.X+$spring.Width/2;$springCentreY=$spring.Y+$spring.Height/2
$dx=$centreX-$springCentreX;$dy=$centreY-$springCentreY
$localX=($dx*$ux)+($dy*$uy);$localY=($dx*$vx)+($dy*$vy)
$normalX=-$vx;$normalY=-$vy
$approach=($velocityX*$normalX)+($velocityY*$normalY)
$topDistance=[Math]::Abs($localY+($spring.Height/2))
$touchesTop=[Math]::Abs($localX)-le($spring.Width/2+$radius*.55)-and$localY-le0-and$topDistance-le($radius+10)
if(-not$touchesTop-or$approach-gt1.5){return $null}
return [pscustomobject]@{X=$normalX;Y=$normalY}
}
function Update-PlayroomGadgets {
if(-not$script:playroomActive-and-not$script:active){return}
Update-WiringSignals
if($script:playroomActive-or($script:inputTick%2)-eq0){Update-ConveyorVisuals}
Update-FreeRotationHandlePosition
Update-DynamicHazards
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_ROTATOR-eq'1'-and-not$script:rotatorUpdateMarker){
[Console]::Out.WriteLine("diagnostic:gadget-update count=$($script:playroomGadgets.Count) kinds=$(@($script:playroomGadgets|ForEach-Object{$_.Kind})-join',')")
$script:rotatorUpdateMarker=$true
}
if($script:teleportCooldown-gt0){$script:teleportCooldown--};if($script:p2TeleportCooldown-gt0){$script:p2TeleportCooldown--}
foreach($object in @($script:playroomBalls)){if($object.TeleportCooldown-gt0){$object.TeleportCooldown--}}
$allPortals=@($script:playroomGadgets|Where-Object{$_.Kind-like'Teleporter*'})
foreach($portal in $allPortals){$portalOn=Test-HazardWiredOn $portal;if($null-ne$portal.Element){$portal.Element.Opacity=$(if($portalOn){1}else{.42})}}
$portals=@($allPortals|Where-Object{Test-HazardWiredOn $_})
foreach($g in @($script:playroomGadgets)){
if($g.Kind-eq'Spring'){
$springTriggered=$false
$p1Spring=Get-SpringTopContact $g ($script:x+$PetWidth/2) ($script:y+$PetHeight/2) ([Math]::Min($PetWidth,$PetHeight)*.38) $script:vx $script:vy
if($null-ne$p1Spring){
$script:vx=$p1Spring.X*24;$script:vy=$p1Spring.Y*24;$script:grounded=$false
$springTriggered=$true
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_SPRING_ALL-eq'1'-and-not$script:springAllP1Reported){
$script:springAllP1Reported=$true;[Console]::Out.WriteLine("diagnostic:spring-all target=Player1 vx=$([Math]::Round($script:vx,2)) vy=$([Math]::Round($script:vy,2))")
}
}
if($script:twoPlayerActive){
$p2Spring=Get-SpringTopContact $g ($script:p2X+$PetWidth/2) ($script:p2Y+$PetHeight/2) ([Math]::Min($PetWidth,$PetHeight)*.38) $script:p2VX $script:p2VY
if($null-ne$p2Spring){
$script:p2VX=$p2Spring.X*24;$script:p2VY=$p2Spring.Y*24;$script:p2Grounded=$false
$springTriggered=$true
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_SPRING_ALL-eq'1'-and-not$script:springAllP2Reported){
$script:springAllP2Reported=$true;[Console]::Out.WriteLine("diagnostic:spring-all target=Player2 vx=$([Math]::Round($script:p2VX,2)) vy=$([Math]::Round($script:p2VY,2))")
}
}
}
foreach($o in @($script:playroomBalls)){
if($o.CarriedBy){continue}
$ballSpring=Get-SpringTopContact $g ($o.X+$o.Size/2) ($o.Y+$o.Size/2) ($o.Size/2) $o.VX $o.VY
if($null-ne$ballSpring){
$o.VX=$ballSpring.X*19;$o.VY=$ballSpring.Y*19
$springTriggered=$true
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_SPRING_ALL-eq'1'-and($null-eq$o.PSObject.Properties['SpringTestReported']-or-not$o.SpringTestReported)){
$o|Add-Member -NotePropertyName SpringTestReported -NotePropertyValue $true -Force
[Console]::Out.WriteLine("diagnostic:spring-all target=$($o.Kind) vx=$([Math]::Round($o.VX,2)) vy=$([Math]::Round($o.VY,2))")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_SPRING_DIRECTION-eq'1'){[Console]::Out.WriteLine("diagnostic:spring-ball-launch angle=$($g.Angle) vx=$($o.VX) vy=$($o.VY)")}
}
}
foreach($hazard in @($script:playroomGadgets|Where-Object{$null-ne$_.PSObject.Properties['IsDynamic']-and$_.IsDynamic})){
if($null-ne$hazard.PSObject.Properties['FrozenRope']-and$null-ne$hazard.FrozenRope){continue}
if($hazard.Dragging){continue}
if($hazard.SpringCooldown-gt0){continue}
$hazardCentreX=$hazard.X+$hazard.BodyOffsetX+($hazard.BodyWidth/2)
$hazardCentreY=$hazard.Y+$hazard.BodyOffsetY+($hazard.BodyHeight/2)
$hazardRadius=[Math]::Min($hazard.BodyWidth,$hazard.BodyHeight)*.46
$hazardSpring=Get-SpringTopContact $g $hazardCentreX $hazardCentreY $hazardRadius $hazard.VX $hazard.VY
if($null-ne$hazardSpring){
$springCentreX=$g.X+($g.Width/2);$springCentreY=$g.Y+($g.Height/2)
$launchCentreX=$springCentreX+($hazardSpring.X*(($g.Height/2)+$hazardRadius+2))
$launchCentreY=$springCentreY+($hazardSpring.Y*(($g.Height/2)+$hazardRadius+2))
$hazard.X=$launchCentreX-$hazard.BodyOffsetX-($hazard.BodyWidth/2)
$hazard.Y=$launchCentreY-$hazard.BodyOffsetY-($hazard.BodyHeight/2)
$hazard.VX=$hazardSpring.X*19;$hazard.VY=$hazardSpring.Y*19
$hazard.SpringCooldown=8
Set-PlacementWindowPosition $hazard
$springTriggered=$true
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_SPRING_ALL-eq'1'-and($null-eq$hazard.PSObject.Properties['SpringTestReported']-or-not$hazard.SpringTestReported)){
$hazard|Add-Member -NotePropertyName SpringTestReported -NotePropertyValue $true -Force
[Console]::Out.WriteLine("diagnostic:spring-all target=$($hazard.Kind) vx=$([Math]::Round($hazard.VX,2)) vy=$([Math]::Round($hazard.VY,2))")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_SPRING_HAZARD-eq'1'){[Console]::Out.WriteLine("diagnostic:spring-hazard-launch kind=$($hazard.Kind) angle=$($g.Angle) vx=$($hazard.VX) vy=$($hazard.VY)")}
}
}
if($springTriggered){$g.CompressionTicks=14}
if($g.CompressionTicks-gt0-and$null-ne$g.SpringScale){
$springPhase=(15-$g.CompressionTicks)/14.0;$springPhase=[Math]::Max(0,[Math]::Min(1,$springPhase))
if($springPhase-lt.34){$phaseValue=$springPhase/.34;$smooth=$phaseValue*$phaseValue*(3-(2*$phaseValue));$springPress=$smooth}
else{$phaseValue=($springPhase-.34)/.66;$smooth=$phaseValue*$phaseValue*(3-(2*$phaseValue));$springPress=1-$smooth}
$g.SpringScale.ScaleY=1-(.18*$springPress)
$g.SpringScale.ScaleX=1+(.055*$springPress)
if($null-ne$g.SpringBase){$g.SpringBase.Opacity=.88+(.12*$springPress)}
$g.CompressionTicks--
if($g.CompressionTicks-le0){$g.SpringScale.ScaleX=1;$g.SpringScale.ScaleY=1;if($null-ne$g.SpringBase){$g.SpringBase.Opacity=1}}
}
}elseif($g.Kind-eq'Fan'){
$fanOn=Test-HazardWiredOn $g
if($null-ne$g.Art){$g.Art.Opacity=$(if($fanOn){1}else{.42})}
Update-FanWind $g $fanOn
if(-not$fanOn){continue}
$r=$g.Angle*[Math]::PI/180;$fx=[Math]::Cos($r);$fy=[Math]::Sin($r)
$originX=$g.X+($g.Width/2);$originY=$g.Y+($g.Height/2)
$p1dx=($script:x+$PetWidth/2)-$originX;$p1dy=($script:y+$PetHeight/2)-$originY
$forward=($p1dx*$fx)+($p1dy*$fy);$side=[Math]::Abs((-1*$p1dx*$fy)+($p1dy*$fx))
if($forward-ge0-and$forward-le280-and$side-le85){$script:vx+=$fx*.32;$script:vy+=$fy*.32}
if($script:twoPlayerActive){
$p2dx=($script:p2X+$PetWidth/2)-$originX;$p2dy=($script:p2Y+$PetHeight/2)-$originY
$forward=($p2dx*$fx)+($p2dy*$fy);$side=[Math]::Abs((-1*$p2dx*$fy)+($p2dy*$fx))
if($forward-ge0-and$forward-le280-and$side-le85){$script:p2VX+=$fx*.32;$script:p2VY+=$fy*.32}
}
foreach($o in @($script:playroomBalls)){
$odx=($o.X+$o.Size/2)-$originX;$ody=($o.Y+$o.Size/2)-$originY
$forward=($odx*$fx)+($ody*$fy);$side=[Math]::Abs((-1*$odx*$fy)+($ody*$fx))
if($forward-ge0-and$forward-le280-and$side-le85){$o.VX+=$fx*.24;$o.VY+=$fy*.24}
}
foreach($hazard in @($script:playroomGadgets|Where-Object{$null-ne$_.PSObject.Properties['IsDynamic']-and$_.IsDynamic-and$_-ne$g})){
if($hazard.Dragging-or($null-ne$hazard.PSObject.Properties['FrozenRope']-and$null-ne$hazard.FrozenRope)){continue}
$hx=$hazard.X+$hazard.BodyOffsetX+($hazard.BodyWidth/2);$hy=$hazard.Y+$hazard.BodyOffsetY+($hazard.BodyHeight/2)
$hdx=$hx-$originX;$hdy=$hy-$originY
$forward=($hdx*$fx)+($hdy*$fy);$side=[Math]::Abs((-1*$hdx*$fy)+($hdy*$fx))
if($forward-ge0-and$forward-le280-and$side-le85){$hazard.VX+=$fx*.24;$hazard.VY+=$fy*.24}
}
}elseif($g.Kind-eq'Rotator'){
$rotatorOn=Test-HazardWiredOn $g
if($null-ne$g.Element){$g.Element.Opacity=$(if($rotatorOn){1}else{.42})}
if(-not$rotatorOn){continue}
$g.SpinAngle=($g.SpinAngle+(2.8*$g.Direction*$g.RotationSpeed))%360
if($null-ne$g.SpinTransform){$g.SpinTransform.Angle=$g.SpinAngle}
if($null-ne$g.Target-and-not$g.Target.Window.IsVisible){$g.Target=$null}
if($null-eq$g.Target){
$nearest=$null;$nearestDistance=125.0;$gx=$g.X+$g.Width/2;$gy=$g.Y+$g.Height/2
$candidateCount=0
foreach($candidate in @($script:playroomBlocks|Where-Object{$null-eq$_.PSObject.Properties['FrozenRope']-or$null-eq$_.FrozenRope})+@($script:playroomGadgets|Where-Object{$_-ne$g-and$_.Kind-notin@('Rotator','Rope')-and($null-eq$_.PSObject.Properties['FrozenRope']-or$null-eq$_.FrozenRope)})){
$candidateCount++
$cx=$candidate.X+$candidate.Width/2;$cy=$candidate.Y+$candidate.Height/2
$distance=[Math]::Sqrt((($cx-$gx)*($cx-$gx))+(($cy-$gy)*($cy-$gy)))
if($distance-lt$nearestDistance){$nearest=$candidate;$nearestDistance=$distance}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and-not$g.DebugSeen){[Console]::Out.WriteLine("diagnostic:rotator-search candidates=$candidateCount nearest=$nearestDistance gx=$gx gy=$gy");$g.DebugSeen=$true}
$g.Target=$nearest
if($null-ne$nearest){
if($null-eq$nearest.PSObject.Properties['ContinuousRotation']){$nearest|Add-Member -NotePropertyName ContinuousRotation -NotePropertyValue $true}
else{$nearest.ContinuousRotation=$true}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:rotator-linked kind=$($nearest.Kind) type=$($nearest.Type)")}
}
}
if($null-ne$g.Target-and($script:inputTick%4)-eq0){
Set-PlacementAngle $g.Target ($g.Target.Angle+(2.8*$g.Direction*$g.RotationSpeed))
if(-not$script:playroomActive-and$env:WINDOWISP_DIAGNOSTICS-eq'1'-and-not$g.GameMarker){[Console]::Out.WriteLine("diagnostic:rotator-game-active angle=$($g.Target.Angle)");$g.GameMarker=$true}
}
}
}
Update-PlayroomRopes
if($portals.Count-ge2){
foreach($source in $portals){$targets=@($portals|Where-Object{$_.Kind-ne$source.Kind}|Select-Object -First 1);if($targets.Count-eq0){continue};$target=$targets[0]
if($script:teleportCooldown-le0-and$script:x+$PetWidth-gt$source.X-and$script:x-lt$source.X+$source.Width-and$script:y+$PetHeight-gt$source.Y-and$script:y-lt$source.Y+$source.Height){$script:x=$target.X+($target.Width-$PetWidth)/2;$script:y=$target.Y-$PetHeight-4;$script:teleportCooldown=45}
if($script:twoPlayerActive-and$script:p2TeleportCooldown-le0-and$script:p2X+$PetWidth-gt$source.X-and$script:p2X-lt$source.X+$source.Width-and$script:p2Y+$PetHeight-gt$source.Y-and$script:p2Y-lt$source.Y+$source.Height){$script:p2X=$target.X+($target.Width-$PetWidth)/2;$script:p2Y=$target.Y-$PetHeight-4;$script:p2TeleportCooldown=45}
foreach($o in @($script:playroomBalls)){if($o.TeleportCooldown-le0-and$o.X+$o.Size-gt$source.X-and$o.X-lt$source.X+$source.Width-and$o.Y+$o.Size-gt$source.Y-and$o.Y-lt$source.Y+$source.Height){$o.X=$target.X+($target.Width-$o.Size)/2;$o.Y=$target.Y-$o.Size-4;$o.TeleportCooldown=45}}
}
}
}
function Clear-PlayroomObjects {
Close-FreeRotationHandle
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){Write-Output "diagnostic:sandbox-clear blocks=$($script:playroomBlocks.Count) balls=$($script:playroomBalls.Count)"}
foreach ($blockData in @($script:playroomBlocks)) { $blockData.Window.Close() }
foreach ($ball in @($script:playroomBalls)) { $ball.Window.Close() }
foreach ($gadget in @($script:playroomGadgets)) {
$gadget.Window.Close()
if($null-ne$gadget.PSObject.Properties['BeamWindow']-and$null-ne$gadget.BeamWindow){$gadget.BeamWindow.Close()}
if($gadget.Kind-eq'Rope'-and$null-ne$script:ropeOverlayCanvas){foreach($ropePath in @($gadget.VisualPaths)){$script:ropeOverlayCanvas.Children.Remove($ropePath)|Out-Null}}
if($null-ne$gadget.PSObject.Properties['WindLines']-and$null-ne$script:ropeOverlayCanvas){foreach($wind in @($gadget.WindLines)){$script:ropeOverlayCanvas.Children.Remove($wind.Line)|Out-Null}}
}
foreach($wire in @($script:playroomWires)){$wire.Window.Close();if($null-ne$script:ropeOverlayCanvas){foreach($wirePath in @($wire.VisualPaths)){$script:ropeOverlayCanvas.Children.Remove($wirePath)|Out-Null}}}
$script:playroomBlocks.Clear(); $script:playroomBalls.Clear();$script:playroomGadgets.Clear();$script:playroomWires.Clear()
$script:ropeConnectFirst=$null;$script:ropeConnectSource=$null
$script:wireConnectFirst=$null
}
function Clear-PlayroomBalls {
foreach ($ball in @($script:playroomBalls)) { Remove-RopesForPlacement $ball; $ball.Window.Close() }
$script:playroomBalls.Clear()
}
function Update-BallPhysicsUi {
$percent = [Math]::Max(1,[Math]::Min(100,[Math]::Round($script:ballPhysicsValue)))
if($null-ne$script:ballPhysicsLabel){$script:ballPhysicsLabel.Text = "BALL BOUNCE $percent%"}
if($null-ne$script:sandboxPhysicsLabel){$script:sandboxPhysicsLabel.Text = "BALL BOUNCE $percent%"}
if($null-ne$script:ballPhysicsSlider-and[math]::Abs($script:ballPhysicsSlider.Value-$script:ballPhysicsValue)-gt0.1){$script:ballPhysicsSlider.Value=$script:ballPhysicsValue}
if($null-ne$script:sandboxPhysicsSlider-and[math]::Abs($script:sandboxPhysicsSlider.Value-$script:ballPhysicsValue)-gt0.1){$script:sandboxPhysicsSlider.Value=$script:ballPhysicsValue}
}
function Toggle-BallCarry([string]$owner) {
$carried = @($script:playroomBalls | Where-Object { $_.CarriedBy -eq $owner } | Select-Object -First 1)
$playerX = $(if ($owner -eq 'Player2') { $script:p2X } else { $script:x })
$playerY = $(if ($owner -eq 'Player2') { $script:p2Y } else { $script:y })
$playerVX = $(if ($owner -eq 'Player2') { $script:p2VX } else { $script:vx })
$playerVY = $(if ($owner -eq 'Player2') { $script:p2VY } else { $script:vy })
$facing = $(if ($owner -eq 'Player2') { $script:p2Facing } else { $script:facing })
if ($facing -eq 0) { $facing = 1 }
if ($carried.Count -gt 0) {
$ball = $carried[0]
$ball.CarriedBy = ''
if($ball.Kind-eq'Bat'){$ball.BatCharging=$false;$ball.BatChargeTicks=0;$ball.SwingTicks=0}
$ball.VX = ($facing * (9.0 + ([Math]::Abs($playerVX) * 0.65)))
$ball.VY = -[Math]::Max(5.5, 6.5 + ([Math]::Max(0,-$playerVY) * 0.35))
$ball.PetHitCooldown = 14
return
}
$nearest = $null; $nearestDistance = 115.0
$playerCenterX = $playerX + ($PetWidth / 2); $playerCenterY = $playerY + ($PetHeight / 2)
foreach ($ball in @($script:playroomBalls)) {
if($null-ne$ball.PSObject.Properties['FrozenRope']-and$null-ne$ball.FrozenRope){continue}
if($null-ne$ball.PSObject.Properties['Dragging']-and$ball.Dragging){$ball.VX=0;$ball.VY=0;continue}
if ($ball.CarriedBy) { continue }
$dx = ($ball.X + $ball.Radius) - $playerCenterX
$dy = ($ball.Y + $ball.Radius) - $playerCenterY
$distance = [Math]::Sqrt(($dx * $dx) + ($dy * $dy))
if ($distance -lt $nearestDistance) { $nearest = $ball; $nearestDistance = $distance }
}
if ($null -ne $nearest) {
$nearest.CarriedBy = $owner; $nearest.VX = 0; $nearest.VY = 0
if($nearest.Kind-eq'Bat'){$nearest.BatCharging=$false;$nearest.BatChargeTicks=0;$nearest.SwingTicks=0}
$nearest.PetHitCooldown = 14
}
}
function Get-CarriedBat([string]$owner) {
$carriedBats=@($script:playroomBalls|Where-Object{$_.Kind-eq'Bat'-and$_.CarriedBy-eq$owner}|Select-Object -First 1)
if($carriedBats.Count-gt0){return $carriedBats[0]}
return $null
}
function Set-PlayroomObjectVisual($object,[double]$angle,[double]$scaleX=1.0) {
if($null-ne$object.PSObject.Properties['VisualRotate']-and$null-ne$object.VisualRotate){
if($null-ne$object.PSObject.Properties['VisualScale']-and$null-ne$object.VisualScale){$object.VisualScale.ScaleX=$scaleX}
$object.VisualRotate.Angle=$angle
}else{$object.Element.RenderTransform.Angle=$angle}
}
function Update-CarriedToyPositions {
foreach($toy in @($script:playroomBalls|Where-Object{$_.CarriedBy})){
$carrierX=$(if($toy.CarriedBy-eq'Player2'){$script:p2X}else{$script:x})
$carrierY=$(if($toy.CarriedBy-eq'Player2'){$script:p2Y}else{$script:y})
$carrierFacing=$(if($toy.CarriedBy-eq'Player2'){$script:p2Facing}else{$script:facing})
if($carrierFacing-eq0){$carrierFacing=1}
$heldDistance=$(if($toy.Kind-eq'Bat'){48}else{42})
$toy.X=$carrierX+($PetWidth/2)+($carrierFacing*$heldDistance)-$toy.Radius
$toy.Y=$carrierY+$(if($toy.Kind-eq'Bat'){15}else{22})
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$toy.X=[Math]::Max($left,[Math]::Min($left+[Windows.SystemParameters]::VirtualScreenWidth-$toy.Size,$toy.X))
$toy.Y=[Math]::Max($top,[Math]::Min($top+[Windows.SystemParameters]::VirtualScreenHeight-$toy.Size,$toy.Y))
$toy.Window.Left=$toy.X;$toy.Window.Top=$toy.Y
}
}
function Hit-BallsWithBat([string]$owner,$bat) {
$playerX=$(if($owner-eq'Player2'){$script:p2X}else{$script:x})
$playerY=$(if($owner-eq'Player2'){$script:p2Y}else{$script:y})
$playerVX=$(if($owner-eq'Player2'){$script:p2VX}else{$script:vx})
$facing=$(if($owner-eq'Player2'){$script:p2Facing}else{$script:facing});if($facing-eq0){$facing=1}
$power=[Math]::Max(0,[Math]::Min(1,$bat.SwingPower))
$strikeX=$playerX+($PetWidth/2)+($facing*(66+(12*$power)));$strikeY=$playerY+46
$hitRadius=94+(18*$power)
foreach($ball in @($script:playroomBalls|Where-Object{$_.Kind-eq'Ball'-and-not$_.CarriedBy})){
$dx=($ball.X+$ball.Radius)-$strikeX;$dy=($ball.Y+$ball.Radius)-$strikeY
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_BAT_SWING-eq'1'){[Console]::Out.WriteLine("diagnostic:bat-contact dx=$dx dy=$dy d2=$(($dx*$dx)+($dy*$dy))")}
if((($dx*$dx)+($dy*$dy))-le($hitRadius*$hitRadius)){
$ball.VX=$facing*(12.0+(18*$power)+([Math]::Abs($playerVX)*.45))
$ball.VY=-(5.5+(7*$power))
$ball.PetHitCooldown=18
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:bat-hit owner=$owner vx=$($ball.VX) vy=$($ball.VY)")}
}
}
$bat.SwingHitDone=$true
}
function Start-BatCharge([string]$owner) {
$bat=Get-CarriedBat $owner
if($null-eq$bat){return $false}
if($bat.SwingTicks-gt0){return $true}
$bat.BatCharging=$true;$bat.BatChargeTicks=0;$bat.SwingHitDone=$false
return $true
}
function Update-BatCharge([string]$owner) {
$bat=Get-CarriedBat $owner
if($null-ne$bat-and$bat.BatCharging){$bat.BatChargeTicks=[Math]::Min(75,$bat.BatChargeTicks+1)}
}
function Release-BatSwing([string]$owner) {
$bat=Get-CarriedBat $owner
if($null-eq$bat-or-not$bat.BatCharging){return $false}
$bat.BatCharging=$false;$bat.SwingPower=[Math]::Max(.08,[Math]::Min(1,$bat.BatChargeTicks/75.0))
$bat.SwingDuration=18;$bat.SwingTicks=$bat.SwingDuration;$bat.SwingHitDone=$false
return $true
}
function Update-PlayroomBalls {
if (-not $script:playroomActive) { return }
$left = [Windows.SystemParameters]::VirtualScreenLeft
$top = [Windows.SystemParameters]::VirtualScreenTop
$right = $left + [Windows.SystemParameters]::VirtualScreenWidth
$floor = $top + [Windows.SystemParameters]::VirtualScreenHeight
foreach ($ball in @($script:playroomBalls)) {
if($null-ne$ball.PSObject.Properties['FrozenRope']-and$null-ne$ball.FrozenRope){continue}
if($script:footballActive-and$null-ne$ball.PSObject.Properties['FootballBall']-and$ball.FootballBall-and$script:footballCountdownTicks-gt0){
$ball.VX=0;$ball.VY=0;$ball.Window.Left=$ball.X;$ball.Window.Top=$ball.Y
continue
}
$size = $ball.Size
if ($ball.CarriedBy) {
$carrierFacing = $(if ($ball.CarriedBy -eq 'Player2') { $script:p2Facing } else { $script:facing })
if ($carrierFacing -eq 0) { $carrierFacing = 1 }
$ball.VX = 0; $ball.VY = 0
if($ball.Kind-eq'Bat'){
if($ball.BatCharging){
$chargePower=[Math]::Min(1,$ball.BatChargeTicks/75.0)
$chargePulse=[Math]::Sin($ball.BatChargeTicks*.48)*2.2*$chargePower
$ball.Angle=$carrierFacing*(-18-(48*$chargePower)+$chargePulse)
Set-PlayroomObjectVisual $ball $ball.Angle ($carrierFacing*(1+(.07*$chargePower)))
}elseif($ball.SwingTicks-gt0){
$swingProgress=($ball.SwingDuration-$ball.SwingTicks)/[double]$ball.SwingDuration
$ball.Angle=$carrierFacing*(-66+(142*$swingProgress))
if(-not$ball.SwingHitDone-and$swingProgress-ge.46){Hit-BallsWithBat $ball.CarriedBy $ball}
$ball.SwingTicks--
Set-PlayroomObjectVisual $ball $ball.Angle $carrierFacing
}else{$ball.Angle=-12*$carrierFacing;Set-PlayroomObjectVisual $ball $ball.Angle $carrierFacing}
}else{
$ball.Angle = ($ball.Angle + ($carrierFacing * 1.2)) % 360
Set-PlayroomObjectVisual $ball $ball.Angle
}
continue
}
$physics = ([Math]::Max(1,[Math]::Min(100,$ball.Physics)) - 1.0) / 99.0
$gravity = 0.82 * [Math]::Pow(1.0 - $physics, 0.35)
$restitution = 0.25 + (0.75 * $physics)
$wallRestitution = 0.55 + (0.45 * $physics)
$kickScale = 0.80 + (0.45 * $physics)
if ($ball.PetHitCooldown -gt 0) { $ball.PetHitCooldown-- }
$oldX = $ball.X; $oldY = $ball.Y
$oldBottom = $ball.Y + $size
$ball.VY = [Math]::Min(20, $ball.VY + $gravity)
$ball.X += $ball.VX; $ball.Y += $ball.VY
if ($ball.X -le $left) { $ball.X = $left; $ball.VX = [Math]::Abs($ball.VX) * $wallRestitution }
if ($ball.X + $size -ge $right) { $ball.X = $right - $size; $ball.VX = -[Math]::Abs($ball.VX) * $wallRestitution }
if ($ball.Y -le $top -and $ball.VY -lt 0) {
$ball.Y = $top; $ball.VY = [Math]::Abs($ball.VY) * $wallRestitution
}
if ($ball.Y + $size -ge $floor) {
$impact = [Math]::Abs($ball.VY)
$ball.Y = $floor - $size
$ball.VY = $(if (($impact * $restitution) -lt 1.0) { 0.0 } else { -$impact * $restitution })
$ball.VX *= $(0.94 + (0.058 * $physics))
}
foreach ($rect in $script:platforms) {
if ($ball.VY -ge 0 -and $ball.X + $size - 4 -gt $rect.Left -and $ball.X + 4 -lt $rect.Right -and
$oldBottom -le ($rect.Top + 5) -and $ball.Y + $size -ge $rect.Top) {
$impact = [Math]::Abs($ball.VY)
$ball.Y = $rect.Top - $size
$ball.VY = $(if (($impact * $restitution) -lt 1.0) { 0.0 } else { -$impact * $restitution })
}
}
foreach ($blockData in @($script:playroomBlocks)) {
if($blockData.Type-eq'Ladder'-or$blockData.TemporaryState-eq'Gone'){continue}
$blockLeft = $blockData.X; $blockRight = $blockData.X + $blockData.Width
$blockTop = $blockData.Y; $blockBottom = $blockData.Y + $blockData.Height
$oldLeft = $oldX; $oldRight = $oldX + $size; $oldTop = $oldY
$newLeft = $ball.X; $newRight = $ball.X + $size
$newTop = $ball.Y; $newBottom = $ball.Y + $size
$horizontalOverlap = $newRight - 3 -gt $blockLeft -and $newLeft + 3 -lt $blockRight
$verticalOverlap = $newBottom - 3 -gt $blockTop -and $newTop + 3 -lt $blockBottom
$surfaceScale = $(if ($blockData.Type -eq 'Bouncy') { 1.18 } elseif ($blockData.Type -eq 'Fire') { 0.95 } elseif ($blockData.Type -eq 'Ice') { 0.82 } else { 1.0 })
$surfaceRestitution = [Math]::Min(1.16, $restitution * $surfaceScale)
if([Math]::Abs($blockData.Angle)-gt.01){
# Sweep the ball along this frame's path so fast balls cannot tunnel
# through the narrower silhouette of an angled or rotating block.
$targetX=$ball.X;$targetY=$ball.Y
$travel=[Math]::Max([Math]::Abs($targetX-$oldX),[Math]::Abs($targetY-$oldY))
$sweepSteps=[Math]::Max(1,[Math]::Min(10,[Math]::Ceiling($travel/4.0)))
$rotatedHit=$false
for($sweepStep=1;$sweepStep-le$sweepSteps;$sweepStep++){
$sweepT=$sweepStep/[double]$sweepSteps
$ball.X=$oldX+(($targetX-$oldX)*$sweepT)
$ball.Y=$oldY+(($targetY-$oldY)*$sweepT)
if(Resolve-BallRotatedBlock $ball $blockData $surfaceRestitution){
$rotatedHit=$true
break
}
}
if(-not$rotatedHit){$ball.X=$targetX;$ball.Y=$targetY}
continue
}
if ($ball.VY -ge 0 -and $horizontalOverlap -and $oldBottom -le $blockTop + 4 -and $newBottom -ge $blockTop) {
$ball.Y = $blockData.Y - $size
Trigger-TemporaryBlock $blockData ("Toy:{0}" -f [Runtime.CompilerServices.RuntimeHelpers]::GetHashCode($ball))
$impact = [Math]::Abs($ball.VY)
$ball.VY = $(if (($impact * $surfaceRestitution) -lt 1.0) { 0.0 } else { -$impact * $surfaceRestitution })
if ($blockData.Type -eq 'Fire') { $ball.VY = [Math]::Min(-6.5, $ball.VY) }
if ($blockData.Type -eq 'Ice') { $ball.VX *= 1.08 }
if ($blockData.Type -eq 'Conveyor') { $ball.VX = [Math]::Max(-16, [Math]::Min(16, $ball.VX + ($blockData.Direction * .85))) }
} elseif($blockData.Type-eq'Cloud'){
# Like the player, toys only see the cloud's descending top
# face; its underside and edges remain airy.
continue
} elseif ($ball.VY -lt 0 -and $horizontalOverlap -and $oldTop -ge $blockBottom - 4 -and $newTop -le $blockBottom) {
$ball.Y = $blockBottom
$ball.VY = [Math]::Abs($ball.VY) * $surfaceRestitution
if ($blockData.Type -eq 'Fire') { $ball.VY = [Math]::Max(6.5, $ball.VY) }
} elseif ($verticalOverlap -and $ball.VX -gt 0 -and $oldRight -le $blockLeft + 4 -and $newRight -ge $blockLeft) {
$ball.X = $blockLeft - $size
$ball.VX = -[Math]::Abs($ball.VX) * $surfaceRestitution
if ($blockData.Type -eq 'Fire') { $ball.VX = [Math]::Min(-6.5, $ball.VX) }
} elseif ($verticalOverlap -and $ball.VX -lt 0 -and $oldLeft -ge $blockRight - 4 -and $newLeft -le $blockRight) {
$ball.X = $blockRight
$ball.VX = [Math]::Abs($ball.VX) * $surfaceRestitution
if ($blockData.Type -eq 'Fire') { $ball.VX = [Math]::Max(6.5, $ball.VX) }
}
}
if ($ball.PetHitCooldown -le 0 -and
$ball.X + $size - 2 -gt $script:x + 8 -and $ball.X + 2 -lt $script:x + $PetWidth - 8 -and
$ball.Y + $size - 2 -gt $script:y + 8 -and $ball.Y + 2 -lt $script:y + $PetHeight - 5) {
$direction = $(if (($ball.X + $ball.Radius) -lt ($script:x + ($PetWidth / 2))) { -1 } else { 1 })
$ball.VX = (($script:vx * 0.85) + ($direction * 5.0)) * $kickScale
$ball.VY = -[Math]::Max(2.4, ([Math]::Abs($script:vy) * 0.42 + 2.2) * $kickScale)
$ball.PetHitCooldown = 12
}
if($script:twoPlayerActive-and$ball.PetHitCooldown-le0-and
$ball.X+$size-2-gt$script:p2X+8-and$ball.X+2-lt$script:p2X+$PetWidth-8-and
$ball.Y+$size-2-gt$script:p2Y+8-and$ball.Y+2-lt$script:p2Y+$PetHeight-5){
$direction=$(if(($ball.X+$ball.Radius)-lt($script:p2X+($PetWidth/2))){-1}else{1})
$ball.VX=(($script:p2VX*.85)+($direction*5.0))*$kickScale
$ball.VY=-[Math]::Max(2.4,([Math]::Abs($script:p2VY)*.42+2.2)*$kickScale)
$ball.PetHitCooldown=12
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:p2-ball-kick vx=$([Math]::Round($ball.VX,1)) vy=$([Math]::Round($ball.VY,1))")}
}
$ball.Angle = ($ball.Angle + ($ball.VX * 1.8)) % 360
Set-PlayroomObjectVisual $ball $ball.Angle
$ball.Window.Left = $ball.X; $ball.Window.Top = $ball.Y
}
}
function New-SandboxSlotButton([string]$label, [string]$kind, $brush, [scriptblock]$action) {
$button=New-Object Windows.Controls.Button
$button.Width=82;$button.Height=70;$button.Margin=[Windows.Thickness]::new(4)
$button.Focusable=$false;$button.IsTabStop=$false
$button.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(38,72,99),[Windows.Media.Color]::FromRgb(24,48,72),90)
$button.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(111,174,216))
$button.BorderThickness=[Windows.Thickness]::new(1)
$button.Cursor=[Windows.Input.Cursors]::Hand
$content=New-Object Windows.Controls.StackPanel
$content.VerticalAlignment=[Windows.VerticalAlignment]::Center
$preview=New-ToyboxImage $(if($kind-like'Teleporter*'){'Teleporter'}else{$kind})
if($null-ne$preview){
$preview.Width=48;$preview.Height=36
}elseif($kind-eq'Ball'){
$preview=New-Object Windows.Shapes.Ellipse
$preview.Width=32;$preview.Height=32;$preview.Fill=$brush
$preview.Stroke=[Windows.Media.Brushes]::White;$preview.StrokeThickness=1.5
}elseif($kind-in@('Bat','Bone','Spring','Teleporter','Fan','Rope')){
$preview=New-Object Windows.Controls.TextBlock
$preview.Text=$(if($kind-eq'Bat'){'🏏'}elseif($kind-eq'Bone'){'🦴'}elseif($kind-eq'Spring'){'↥'}elseif($kind-eq'Teleporter'){'◎'}elseif($kind-eq'Rope'){'〰'}else{'🌀'})
$preview.FontFamily=New-Object Windows.Media.FontFamily('Segoe UI Emoji')
$preview.FontSize=27;$preview.Foreground=$brush
}else{
$preview=New-Object Windows.Controls.Border
$preview.Width=46;$preview.Height=22;$preview.Background=$brush
$preview.BorderBrush=[Windows.Media.Brushes]::White;$preview.BorderThickness=[Windows.Thickness]::new(1)
$preview.CornerRadius=[Windows.CornerRadius]::new($(if($kind-eq'Bouncy'){9}else{3}))
}
$preview.HorizontalAlignment=[Windows.HorizontalAlignment]::Center
$content.Children.Add($preview)|Out-Null
$text=New-Object Windows.Controls.TextBlock
$text.Text=$label;$text.Foreground=[Windows.Media.Brushes]::White;$text.FontSize=10
$text.FontWeight=[Windows.FontWeights]::SemiBold;$text.HorizontalAlignment=[Windows.HorizontalAlignment]::Center
$text.Margin=[Windows.Thickness]::new(0,5,0,0)
$content.Children.Add($text)|Out-Null
$button.Content=$content;$button.Add_Click($action)
return $button
}
function Add-SandboxMysterySlots([string]$category) {
# Every palette deliberately keeps three unrevealed spaces. When a future
# item replaces one, add another mystery slot so anticipation never reaches zero.
foreach($mysteryIndex in 1..3){
$button=New-Object Windows.Controls.Button
$button.Width=82;$button.Height=70;$button.Margin=[Windows.Thickness]::new(4)
$button.Tag="Mystery:$category"
$button.Focusable=$false;$button.IsTabStop=$false;$button.Cursor=[Windows.Input.Cursors]::Arrow
$button.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(32,48,64),[Windows.Media.Color]::FromRgb(16,29,43),90)
$button.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(79,105,126));$button.BorderThickness=[Windows.Thickness]::new(1)
$button.ToolTip="Unknown $category item - to be announced"
$content=New-Object Windows.Controls.StackPanel;$content.VerticalAlignment='Center'
$silhouette=New-Object Windows.Controls.Border;$silhouette.Width=48;$silhouette.Height=31;$silhouette.CornerRadius=[Windows.CornerRadius]::new(8)
$silhouette.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(11,23,35));$silhouette.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(71,97,119));$silhouette.BorderThickness=[Windows.Thickness]::new(1)
$question=New-Object Windows.Controls.TextBlock;$question.Text='???';$question.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(137,164,184));$question.FontSize=16;$question.FontWeight='Bold';$question.HorizontalAlignment='Center';$question.VerticalAlignment='Center';$silhouette.Child=$question;$silhouette.HorizontalAlignment='Center';$content.Children.Add($silhouette)|Out-Null
$label=New-Object Windows.Controls.TextBlock;$label.Text='TBA';$label.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(154,184,205));$label.FontSize=10;$label.FontWeight='Bold';$label.HorizontalAlignment='Center';$label.Margin=[Windows.Thickness]::new(0,5,0,0);$content.Children.Add($label)|Out-Null
$button.Content=$content;$script:sandboxTrayPanel.Children.Add($button)|Out-Null
}
}
function Update-SandboxToolboxPosition {
if($null-eq$script:sandboxHotbarWindow){return}
$screenLeft=[Windows.SystemParameters]::VirtualScreenLeft
$screenTop=[Windows.SystemParameters]::VirtualScreenTop
$screenWidth=[Windows.SystemParameters]::VirtualScreenWidth
$screenHeight=[Windows.SystemParameters]::VirtualScreenHeight
if(-not$script:sandboxToolboxHasCustomPosition){
$script:sandboxHotbarWindow.Left=$screenLeft+(($screenWidth-$script:sandboxHotbarWindow.Width)/2)
$script:sandboxHotbarWindow.Top=$screenTop+$screenHeight-$script:sandboxHotbarWindow.Height-24
}
$script:sandboxTrayWindow.Left=$script:sandboxHotbarWindow.Left+(($script:sandboxHotbarWindow.Width-$script:sandboxTrayWindow.Width)/2)
$script:sandboxTrayWindow.Top=$script:sandboxHotbarWindow.Top-$script:sandboxTrayWindow.Height-8
}
function Show-SandboxCategory([string]$category) {
if($null-eq$script:sandboxTrayWindow){return}
if($script:sandboxCategory-eq$category-and$script:sandboxTrayWindow.IsVisible){
$script:sandboxTrayWindow.Hide();$script:sandboxCategory='';return
}
$script:sandboxCategory=$category
$script:sandboxTrayPanel.Children.Clear()
$twoRows=$category-in@('Blocks','Gadgets')
$script:sandboxTrayPanel.Rows=$(if($twoRows){2}else{1})
$script:sandboxTrayWindow.Height=$(if($twoRows){194}else{118})
$script:sandboxTrayTitle.Text=$(if($category-eq'Blocks'){'BLOCK PALETTE'}elseif($category-eq'Toys'){'TOYBOX'}elseif($category-eq'Gadgets'){'GADGET LAB'}else{'HAZARDS'})
if($category-eq'Blocks'){
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Normal' 'Normal' (Get-BlockBrush 'Normal') {Add-PlayroomBlock 'Normal'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Wood Platform' 'Wooden' (Get-BlockBrush 'Wooden') {Add-PlayroomBlock 'Wooden'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Bouncy' 'Bouncy' (Get-BlockBrush 'Bouncy') {Add-PlayroomBlock 'Bouncy'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Fire' 'Fire' (Get-BlockBrush 'Fire') {Add-PlayroomBlock 'Fire'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Ice' 'Ice' (Get-BlockBrush 'Ice') {Add-PlayroomBlock 'Ice'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Grass' 'Grass' (Get-BlockBrush 'Grass') {Add-PlayroomBlock 'Grass'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Mirror' 'Mirror' (Get-BlockBrush 'Mirror') {Add-PlayroomBlock 'Mirror'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Ladder' 'Ladder' (Get-BlockBrush 'Ladder') {Add-PlayroomBlock 'Ladder'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Conveyor' 'Conveyor' (Get-BlockBrush 'Conveyor') {Add-PlayroomBlock 'Conveyor'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Cloud' 'Cloud' (Get-BlockBrush 'Cloud') {Add-PlayroomBlock 'Cloud'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Crumbling' 'Crumbling' (Get-BlockBrush 'Crumbling') {Add-PlayroomBlock 'Crumbling'}))|Out-Null
}elseif($category-eq'Toys'){
$ballBrush=New-Object Windows.Media.LinearGradientBrush
$ballBrush.StartPoint=[Windows.Point]::new(0,0);$ballBrush.EndPoint=[Windows.Point]::new(1,1)
$ballBrush.GradientStops.Add((New-Object Windows.Media.GradientStop ([Windows.Media.Color]::FromRgb(220,53,62),0.0)))
$ballBrush.GradientStops.Add((New-Object Windows.Media.GradientStop ([Windows.Media.Color]::FromRgb(42,105,207),0.52)))
$ballBrush.GradientStops.Add((New-Object Windows.Media.GradientStop ([Windows.Media.Color]::FromRgb(220,53,62),1.0)))
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Ball' 'Ball' $ballBrush {Add-PlayroomBall}))|Out-Null
$wood=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(225,163,74))
$cream=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(244,235,210))
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Baseball Bat' 'Bat' $wood {Add-PlayroomToy 'Bat'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Toy Bone' 'Bone' $cream {Add-PlayroomToy 'Bone'}))|Out-Null
}elseif($category-eq'Gadgets'){
$green=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(112,221,153))
$blue=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(105,184,239))
$violet=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(184,132,246))
$purple=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(210,137,241))
$switchBlue=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(70,134,205))
$buttonRed=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(218,74,91))
$plateGold=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(222,170,54))
$timerPurple=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(145,87,208))
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Spring' 'Spring' $green {Add-PlayroomGadget 'Spring'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Teleporter' 'Teleporter' $violet {Add-PlayroomGadget 'Teleporter'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Fan' 'Fan' $blue {Add-PlayroomGadget 'Fan'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Rotator' 'Rotator' $purple {Add-PlayroomGadget 'Rotator'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Switch' 'Switch' $switchBlue {Add-PlayroomGadget 'Switch'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Button' 'Button' $buttonRed {Add-PlayroomGadget 'Button'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Plate' 'PressurePlate' $plateGold {Add-PlayroomGadget 'PressurePlate'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Timer' 'Timer' $timerPurple {Add-PlayroomGadget 'Timer'}))|Out-Null
}else{
$steel=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(186,196,213))
$orange=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(245,100,36))
$red=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(244,50,75))
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Spikes' 'Spikes' $steel {Add-PlayroomHazard 'Spikes'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Fire Jet' 'FireJet' $orange {Add-PlayroomHazard 'FireJet'}))|Out-Null
$script:sandboxTrayPanel.Children.Add((New-SandboxSlotButton 'Laser' 'Laser' $red {Add-PlayroomHazard 'Laser'}))|Out-Null
}
Add-SandboxMysterySlots $category
Update-SandboxToolboxPosition
if(-not$script:sandboxTrayWindow.IsVisible){$script:sandboxTrayWindow.Show()}
Set-OverlayClickThroughState $script:sandboxTrayWindow $false
$script:sandboxTrayWindow.Topmost=$true
$script:sandboxTrayWindow.Activate()|Out-Null
}
function Set-SandboxToolboxVisible([bool]$visible) {
if($null-eq$script:sandboxHotbarWindow){return}
if($visible){
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){Write-Output 'diagnostic:builder-show'}
Update-SandboxToolboxPosition
if(-not$script:sandboxHotbarWindow.IsVisible){$script:sandboxHotbarWindow.Show()}
Set-OverlayClickThroughState $script:sandboxHotbarWindow $false
$script:sandboxHotbarWindow.Topmost=$true
$script:sandboxHotbarWindow.Activate()|Out-Null
if($script:gridSnapEnabled){New-GridOverlay;if(-not$script:gridOverlayWindow.IsVisible){$script:gridOverlayWindow.Show();Set-OverlayClickThroughState $script:gridOverlayWindow $true}}
}else{
Set-EraserMode $false
Set-RopeToolMode $false;Set-WiringMode $false
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){Write-Output 'diagnostic:builder-hide'}
if($null-ne$script:gridOverlayWindow){$script:gridOverlayWindow.Hide()}
$script:sandboxTrayWindow.Hide();$script:sandboxHotbarWindow.Hide();$script:sandboxCategory=''
}
}
function New-SandboxToolbox {
if($null-ne$script:sandboxHotbarWindow){return}
$script:sandboxHotbarWindow=New-Object Windows.Window
$script:sandboxHotbarWindow.Title='Windowisp Builder Hotbar'
$availableHotbarWidth=[Math]::Max(360,[Windows.SystemParameters]::VirtualScreenWidth-24)
$script:sandboxHotbarWindow.Width=[Math]::Min(854,$availableHotbarWidth);$script:sandboxHotbarWindow.Height=58
$script:sandboxHotbarWindow.WindowStyle=[Windows.WindowStyle]::None
$script:sandboxHotbarWindow.AllowsTransparency=$true
$script:sandboxHotbarWindow.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(28,65,94),[Windows.Media.Color]::FromRgb(15,33,53),90)
$script:sandboxHotbarWindow.Topmost=$true;$script:sandboxHotbarWindow.ShowInTaskbar=$false
$script:sandboxHotbarWindow.ResizeMode=[Windows.ResizeMode]::NoResize
$hotbarRoot=New-Object Windows.Controls.DockPanel
$hotbarGrip=New-Object Windows.Controls.Border
$hotbarGrip.Width=12;$hotbarGrip.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(78,139,177))
$hotbarGrip.CornerRadius=[Windows.CornerRadius]::new(8,0,0,8);$hotbarGrip.Cursor=[Windows.Input.Cursors]::SizeAll
$hotbarGripText=New-Object Windows.Controls.TextBlock
$hotbarGripText.Text=[string][char]0x22EE;$hotbarGripText.Foreground=[Windows.Media.Brushes]::White
$hotbarGripText.FontSize=18;$hotbarGripText.Opacity=.65
$hotbarGripText.HorizontalAlignment='Center';$hotbarGripText.VerticalAlignment='Center'
$hotbarGrip.Child=$hotbarGripText
$hotbarGrip.Add_MouseLeftButtonDown({
$script:sandboxHotbarWindow.DragMove();$script:sandboxToolboxHasCustomPosition=$true
if($null-ne$script:sandboxTrayWindow){
$script:sandboxTrayWindow.Left=$script:sandboxHotbarWindow.Left+(($script:sandboxHotbarWindow.Width-$script:sandboxTrayWindow.Width)/2)
$script:sandboxTrayWindow.Top=$script:sandboxHotbarWindow.Top-$script:sandboxTrayWindow.Height-8
}
})
[Windows.Controls.DockPanel]::SetDock($hotbarGrip,[Windows.Controls.Dock]::Left);$hotbarRoot.Children.Add($hotbarGrip)|Out-Null
$bar=New-Object Windows.Controls.StackPanel;$bar.Orientation=[Windows.Controls.Orientation]::Horizontal
$bar.HorizontalAlignment=[Windows.HorizontalAlignment]::Center;$bar.VerticalAlignment=[Windows.VerticalAlignment]::Center
$blocks=New-MenuButton 'Blocks' {Show-SandboxCategory 'Blocks'} ''
$blocks.Width=94;$blocks.Margin=[Windows.Thickness]::new(3);$blocks.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(83,158,91),[Windows.Media.Color]::FromRgb(37,91,58),90);$blocks.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(139,211,137))
$toys=New-MenuButton 'Toys' {Show-SandboxCategory 'Toys'} ''
$toys.Width=86;$toys.Margin=[Windows.Thickness]::new(3);$toys.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(218,156,59),[Windows.Media.Color]::FromRgb(130,78,29),90);$toys.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,208,116))
$gadgets=New-MenuButton 'Gadgets' {Show-SandboxCategory 'Gadgets'} ''
$gadgets.Width=100;$gadgets.Margin=[Windows.Thickness]::new(3);$gadgets.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(94,111,196),[Windows.Media.Color]::FromRgb(54,61,125),90);$gadgets.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(159,174,244))
$hazards=New-MenuButton 'Hazards' {Show-SandboxCategory 'Hazards'} ''
$hazards.Width=100;$hazards.Margin=[Windows.Thickness]::new(3);$hazards.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(191,67,79),[Windows.Media.Color]::FromRgb(108,34,52),90);$hazards.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(246,135,143))
$script:eraserButton=New-Object Windows.Controls.Button
$script:eraserButton.Width=48;$script:eraserButton.Height=40;$script:eraserButton.Margin=[Windows.Thickness]::new(3)
$script:eraserButton.Focusable=$false;$script:eraserButton.IsTabStop=$false
$script:eraserButton.Cursor=[Windows.Input.Cursors]::Hand
$script:eraserButton.BorderThickness=[Windows.Thickness]::new(1)
$eraserCanvas=New-Object Windows.Controls.Canvas;$eraserCanvas.Width=28;$eraserCanvas.Height=24
$eraserBody=New-Object Windows.Controls.Border;$eraserBody.Width=22;$eraserBody.Height=12
$eraserBody.CornerRadius=[Windows.CornerRadius]::new(2)
$eraserBody.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(255,126,151),[Windows.Media.Color]::FromRgb(106,187,230),0)
$eraserBody.BorderBrush=[Windows.Media.Brushes]::White;$eraserBody.BorderThickness=[Windows.Thickness]::new(1.5)
$eraserBody.RenderTransformOrigin=[Windows.Point]::new(.5,.5);$eraserBody.RenderTransform=[Windows.Media.RotateTransform]::new(-32)
[Windows.Controls.Canvas]::SetLeft($eraserBody,3);[Windows.Controls.Canvas]::SetTop($eraserBody,6)
$eraserCanvas.Children.Add($eraserBody)|Out-Null
$script:eraserButton.Content=$eraserCanvas
$script:eraserButton.Add_Click({Toggle-EraserMode})
Update-EraserButtonVisual
$script:gridButton=New-Object Windows.Controls.Button
$script:gridButton.Width=48;$script:gridButton.Height=40;$script:gridButton.Margin=[Windows.Thickness]::new(3)
$script:gridButton.Focusable=$false;$script:gridButton.IsTabStop=$false;$script:gridButton.Cursor=[Windows.Input.Cursors]::Hand
$script:gridButton.BorderThickness=[Windows.Thickness]::new(1);$script:gridButton.Add_Click({Toggle-GridSnap})
$gridIcon=New-Object Windows.Controls.Canvas;$gridIcon.Width=26;$gridIcon.Height=26
$gridBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(126,225,242))
foreach($offset in @(2.0,9.0,16.0,23.0)){
$vertical=New-Object Windows.Shapes.Line;$vertical.X1=$offset;$vertical.X2=$offset;$vertical.Y1=2;$vertical.Y2=24;$vertical.Stroke=$gridBrush;$vertical.StrokeThickness=1.5;$gridIcon.Children.Add($vertical)|Out-Null
$horizontal=New-Object Windows.Shapes.Line;$horizontal.X1=2;$horizontal.X2=24;$horizontal.Y1=$offset;$horizontal.Y2=$offset;$horizontal.Stroke=$gridBrush;$horizontal.StrokeThickness=1.5;$gridIcon.Children.Add($horizontal)|Out-Null
}
$script:gridButton.Content=$gridIcon;Update-GridButtonVisual
$script:ropeToolButton=New-Object Windows.Controls.Button;$script:ropeToolButton.Width=48;$script:ropeToolButton.Height=40;$script:ropeToolButton.Margin=[Windows.Thickness]::new(3);$script:ropeToolButton.Focusable=$false;$script:ropeToolButton.Cursor=[Windows.Input.Cursors]::Hand;$script:ropeToolButton.ToolTip='Rope Tool';$script:ropeToolButton.Add_Click({Set-RopeToolMode (-not$script:ropeToolMode)})
$ropeIcon=New-Object Windows.Controls.Canvas;$ropeIcon.Width=30;$ropeIcon.Height=26
$ropeShadow=New-Object Windows.Shapes.Path;$ropeShadow.Data=[Windows.Media.Geometry]::Parse('M4,19 C1,10 8,3 15,8 C23,14 13,24 7,18 C2,12 17,5 24,12 C28,17 24,22 20,22');$ropeShadow.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(77,42,22));$ropeShadow.StrokeThickness=6;$ropeShadow.StrokeStartLineCap='Round';$ropeShadow.StrokeEndLineCap='Round'
$ropeLine=New-Object Windows.Shapes.Path;$ropeLine.Data=$ropeShadow.Data;$ropeLine.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(231,169,77));$ropeLine.StrokeThickness=3;$ropeLine.StrokeStartLineCap='Round';$ropeLine.StrokeEndLineCap='Round';$ropeIcon.Children.Add($ropeShadow)|Out-Null;$ropeIcon.Children.Add($ropeLine)|Out-Null;$script:ropeToolButton.Content=$ropeIcon
$script:wiringButton=New-Object Windows.Controls.Button;$script:wiringButton.Width=48;$script:wiringButton.Height=40;$script:wiringButton.Margin=[Windows.Thickness]::new(3);$script:wiringButton.Focusable=$false;$script:wiringButton.Cursor=[Windows.Input.Cursors]::Hand;$script:wiringButton.ToolTip='Wiring Tool';$script:wiringButton.Add_Click({Set-WiringMode (-not$script:wiringMode)})
$wireIcon=New-Object Windows.Controls.Canvas;$wireIcon.Width=30;$wireIcon.Height=26
foreach($ringSize in @(20,14,8)){$ring=New-Object Windows.Shapes.Ellipse;$ring.Width=$ringSize;$ring.Height=$ringSize;$ring.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,70,87));$ring.StrokeThickness=2.5;[Windows.Controls.Canvas]::SetLeft($ring,3+((20-$ringSize)/2));[Windows.Controls.Canvas]::SetTop($ring,3+((20-$ringSize)/2));$wireIcon.Children.Add($ring)|Out-Null}
$wireTail=New-Object Windows.Shapes.Path;$wireTail.Data=[Windows.Media.Geometry]::Parse('M22,13 C27,13 25,23 29,23');$wireTail.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,205,65));$wireTail.StrokeThickness=3;$wireTail.StrokeStartLineCap='Round';$wireTail.StrokeEndLineCap='Round';$wireIcon.Children.Add($wireTail)|Out-Null;$script:wiringButton.Content=$wireIcon
Update-ConnectionToolVisuals
$clear=New-MenuButton 'Clear All' {Clear-PlayroomObjects} ''
$clear.Width=100;$clear.Margin=[Windows.Thickness]::new(3)
$close=New-MenuButton 'Hide' {Set-SandboxToolboxVisible $false} ''
$close.Width=82;$close.Margin=[Windows.Thickness]::new(3)
$bar.Children.Add($blocks)|Out-Null;$bar.Children.Add($toys)|Out-Null;$bar.Children.Add($gadgets)|Out-Null;$bar.Children.Add($hazards)|Out-Null
$bar.Children.Add($script:ropeToolButton)|Out-Null;$bar.Children.Add($script:wiringButton)|Out-Null;$bar.Children.Add($script:eraserButton)|Out-Null;$bar.Children.Add($script:gridButton)|Out-Null
$bar.Children.Add($clear)|Out-Null;$bar.Children.Add($close)|Out-Null
$hotbarScroll=New-Object Windows.Controls.ScrollViewer
$hotbarScroll.HorizontalScrollBarVisibility=[Windows.Controls.ScrollBarVisibility]::Auto;$hotbarScroll.VerticalScrollBarVisibility=[Windows.Controls.ScrollBarVisibility]::Disabled
$hotbarScroll.PanningMode=[Windows.Controls.PanningMode]::HorizontalOnly;$hotbarScroll.Content=$bar
$hotbarRoot.Children.Add($hotbarScroll)|Out-Null
$script:sandboxHotbarWindow.Content=$hotbarRoot
$script:sandboxTrayWindow=New-Object Windows.Window
$script:sandboxTrayWindow.Title='Windowisp Builder Tray'
$script:sandboxTrayWindow.Width=790;$script:sandboxTrayWindow.Height=118
$script:sandboxTrayWindow.WindowStyle=[Windows.WindowStyle]::None
$script:sandboxTrayWindow.AllowsTransparency=$true
$script:sandboxTrayWindow.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(30,62,88),[Windows.Media.Color]::FromRgb(17,36,56),90)
$script:sandboxTrayWindow.Topmost=$true;$script:sandboxTrayWindow.ShowInTaskbar=$false
$script:sandboxTrayWindow.ResizeMode=[Windows.ResizeMode]::NoResize
$trayRoot=New-Object Windows.Controls.DockPanel
$script:sandboxTrayTitle=New-Object Windows.Controls.TextBlock
$script:sandboxTrayTitle.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(190,229,250))
$script:sandboxTrayTitle.FontSize=11;$script:sandboxTrayTitle.FontWeight=[Windows.FontWeights]::Bold
$script:sandboxTrayTitle.Margin=[Windows.Thickness]::new(12,7,0,2)
[Windows.Controls.DockPanel]::SetDock($script:sandboxTrayTitle,[Windows.Controls.Dock]::Top)
$trayRoot.Children.Add($script:sandboxTrayTitle)|Out-Null
$script:sandboxTrayPanel=New-Object Windows.Controls.Primitives.UniformGrid
$script:sandboxTrayPanel.Rows=1;$script:sandboxTrayPanel.HorizontalAlignment=[Windows.HorizontalAlignment]::Stretch
$trayRoot.Children.Add($script:sandboxTrayPanel)|Out-Null
$script:sandboxTrayWindow.Content=$trayRoot
Update-SandboxToolboxPosition
}
function Start-Playroom {
if ($script:playroomActive) { return }
$script:playroomActive = $true
Set-WireEditingVisible $true
Set-RopeEditingVisible $true
if($null-eq$script:sandboxHotbarWindow){New-SandboxToolbox}
Set-SandboxToolboxVisible $false
foreach ($blockData in @($script:playroomBlocks)) { Set-BlockGameplayVisual $blockData $false }
foreach($placement in @($script:playroomGadgets)){
if($placement.Kind-in@('Rotator','Rope')){$placement.Window.Show()}
Set-OverlayClickThroughState $placement.Window $false
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output 'diagnostic:desktop-play-start' }
foreach ($shot in @($script:projectiles)) { Remove-SharedGameplayElement $shot.Element }
$script:projectiles.Clear(); Remove-AllEnemies; Remove-Star $false; Remove-Shield; Remove-ShieldPickup
$script:enemySpawnTicks = 999999; $script:starSpawnTicks = 999999; $script:shieldPickupSpawnTicks = 999999
Set-GameMode $true
# Re-entering Sandbox must preserve the user's build without duplicating
# the starter platform on every round trip through another game mode.
if($script:playroomBlocks.Count-eq0){Add-PlayroomBlock 'Normal'}
if($script:playroomBalls.Count-eq0){Add-PlayroomBall}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_P2_PET_SWITCH-eq'1'){
Set-TwoPlayerMode $true
$p1Before=$script:activePetId
$p2Choice=@(Get-PersonalPetCatalog|Where-Object{$_.Id-ne$script:activePetId}|Select-Object -First 1)
if($p2Choice.Count-gt0){$p2SwitchResult=Select-PetForPlayer $p2Choice[0] 'P2'}else{$p2SwitchResult=$false}
[Console]::Out.WriteLine("diagnostic:p2-direct-switch result=$p2SwitchResult p1=$($script:activePetId) p1Before=$p1Before p2=$($script:p2PetId) p2Name=$($script:p2PetName)")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_ERASER-eq'1'){
$eraserBefore=$script:playroomBlocks.Count
Set-EraserMode $true
$eraserTarget=@($script:playroomBlocks|Select-Object -First 1)
if($eraserTarget.Count-gt0){Remove-PlayroomPlacement $eraserTarget[0]}
Invoke-ExitControl
Invoke-ExitControl
[Console]::Out.WriteLine("diagnostic:eraser active=$($script:eraserMode) gameActive=$($script:active) exitGuard=$($script:eraserExitGuardTicks) before=$eraserBefore after=$($script:playroomBlocks.Count) button=$($null-ne$script:eraserButton)")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_LADDER-eq'1'){
Add-PlayroomBlock 'Ladder'
$testLadder=@($script:playroomBlocks|Where-Object{$_.Type-eq'Ladder'}|Select-Object -Last 1)[0]
if($null-ne$testLadder){
$testLadder.X=$script:x+($PetWidth/2)-($testLadder.Width/2)
$testLadder.Y=$script:y+($PetHeight/2)-($testLadder.Height/2)
Set-PlacementWindowPosition $testLadder
Set-PlacementAngle $testLadder 28
$testContact=Get-LadderContact $script:x $script:y
if($null-ne$testContact){
[Console]::Out.WriteLine("diagnostic:ladder-contact found=True angle=$($testLadder.Angle) upX=$($testContact.UpX) upY=$($testContact.UpY)")
$testSolid=[pscustomobject]@{Type='Normal';X=$testLadder.X;Y=$testLadder.Y;Width=$testLadder.Width;Height=$testLadder.Height}
$testOneWay=[pscustomobject]@{Type='Wooden';X=$testLadder.X;Y=$testLadder.Y;Width=$testLadder.Width;Height=$testLadder.Height}
$solidPass=Test-LadderBlockPassThrough $testContact $testSolid
$oneWayPass=Test-LadderBlockPassThrough $testContact $testOneWay
[Console]::Out.WriteLine("diagnostic:ladder-block-pass solid=$solidPass oneWay=$oneWayPass")
}
else{[Console]::Out.WriteLine("diagnostic:ladder-contact found=False angle=$($testLadder.Angle)")}
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_ROPE-eq'1'){
$testRopeBlock=@($script:playroomBlocks|Select-Object -First 1)[0]
$testRopeBall=@($script:playroomBalls|Select-Object -First 1)[0]
if($null-ne$testRopeBlock-and$null-ne$testRopeBall){
Add-PlayroomGadget 'RopeCoil'
$testRopeCoil=@($script:playroomGadgets|Where-Object{$_.Kind-eq'RopeCoil'}|Select-Object -Last 1)[0]
Arm-RopePlacement $testRopeCoil
Connect-RopeEndpoint $testRopeBlock
Connect-RopeEndpoint $testRopeBall
$testRope=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'}|Select-Object -Last 1)[0]
$testRope.Length=140
$testRopeBall.X=$testRopeBlock.X+300;$testRopeBall.Y=$testRopeBlock.Y+160
$testRopeBall.VX=3;$testRopeBall.VY=4
[Console]::Out.WriteLine('diagnostic:rope-test-started')
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_ROPE_UNATTACH-eq'1'){
$testUnattachBlock=@($script:playroomBlocks|Select-Object -First 1)[0]
$testUnattachBall=@($script:playroomBalls|Select-Object -First 1)[0]
if($null-ne$testUnattachBlock-and$null-ne$testUnattachBall){
New-PlayroomRope $testUnattachBlock $testUnattachBall
$testUnattachRope=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'}|Select-Object -Last 1)[0]
Unattach-PlayroomRope $testUnattachRope
[Console]::Out.WriteLine("diagnostic:rope-unattached ropes=$(@($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'}).Count) coils=$(@($script:playroomGadgets|Where-Object{$_.Kind-eq'RopeCoil'}).Count)")
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_ROPE_STRETCH-eq'1'){
$testStretchBlock=@($script:playroomBlocks|Select-Object -First 1)[0]
$testStretchBall=@($script:playroomBalls|Select-Object -First 1)[0]
if($null-ne$testStretchBlock-and$null-ne$testStretchBall){
New-PlayroomRope $testStretchBlock $testStretchBall
$testStretchRope=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'}|Select-Object -Last 1)[0]
$oldLength=$testStretchRope.Length
$testStretchRope.DragStartLength=$oldLength
$testStretchRope.DragStartX=$testStretchRope.Window.Left+13
$testStretchRope.DragStartY=$testStretchRope.Window.Top+13
$testStretchRope.Dragging=$true
$testStretchRope.Window.Left+=110;$testStretchRope.Window.Top+=170
Set-RopeLengthFromHandle $testStretchRope
$testStretchRope.Dragging=$false
Update-OnePlayroomRope $testStretchRope $true
$longLength=$testStretchRope.Length
$testStretchRope.DragStartLength=$longLength
$testStretchRope.DragStartX=$testStretchRope.Window.Left+13
$testStretchRope.DragStartY=$testStretchRope.Window.Top+13
$testStretchRope.Dragging=$true
$testStretchRope.Window.Top-=260
Set-RopeLengthFromHandle $testStretchRope
$testStretchRope.Dragging=$false
Update-OnePlayroomRope $testStretchRope $true
[Console]::Out.WriteLine("diagnostic:rope-resized old=$oldLength longer=$longLength shorter=$($testStretchRope.Length)")
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_HAZARD_GRAVITY-eq'1'){
Add-PlayroomHazard 'Laser';Add-PlayroomHazard 'FireJet';Add-PlayroomHazard 'Spikes'
$testGravityBlock=@($script:playroomBlocks|Select-Object -First 1)[0]
$testGravityHazards=@($script:playroomGadgets|Where-Object{$_.Kind-in@('Laser','FireJet','Spikes')})
if($null-ne$testGravityBlock){
foreach($testGravityHazard in $testGravityHazards){New-PlayroomRope $testGravityBlock $testGravityHazard 190}
[Console]::Out.WriteLine("diagnostic:hazard-ropes-created count=$($testGravityHazards.Count)")
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_CONVEYOR_HAZARD-eq'1'){
Add-PlayroomBlock 'Conveyor';Add-PlayroomHazard 'FireJet'
$testConveyor=@($script:playroomBlocks|Where-Object{$_.Type-eq'Conveyor'}|Select-Object -Last 1)[0]
$testConveyorHazard=@($script:playroomGadgets|Where-Object{$_.Kind-eq'FireJet'}|Select-Object -Last 1)[0]
$testLeft=[Windows.SystemParameters]::VirtualScreenLeft;$testTop=[Windows.SystemParameters]::VirtualScreenTop
$testConveyor.X=$testLeft+100;$testConveyor.Y=$testTop+350;$testConveyor.Width=300;Set-PlacementAngle $testConveyor 0
$testConveyorHazard.X=$testConveyor.X+40;$testConveyorHazard.Y=$testConveyor.Y-$testConveyorHazard.BodyOffsetY-$testConveyorHazard.BodyHeight
$testConveyorHazard.VX=7;$testConveyorHazard.VY=0;Set-PlacementWindowPosition $testConveyorHazard
[Console]::Out.WriteLine('diagnostic:conveyor-hazard-started')
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_WIRING-eq'1'){
foreach($kind in @('Switch','Button','PressurePlate','Timer')){Add-PlayroomGadget $kind}
Add-PlayroomGadget 'Fan'
foreach($kind in @('Spikes','FireJet','Laser')){Add-PlayroomHazard $kind}
$testInputs=@($script:playroomGadgets|Where-Object{$_.Kind-in@('Switch','Button','PressurePlate','Timer')})
$testFan=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Fan'}|Select-Object -Last 1)[0]
$testHazards=@($script:playroomGadgets|Where-Object{$_.Kind-in@('Spikes','FireJet','Laser')})
Update-PlayroomHazards;Update-PlayroomGadgets
$testFire=@($testHazards|Where-Object{$_.Kind-eq'FireJet'})[0];$testLaser=@($testHazards|Where-Object{$_.Kind-eq'Laser'})[0]
[Console]::Out.WriteLine("diagnostic:wiring-defaults spike=$(Test-HazardWiredOn $testHazards[0]) fire=$(Test-HazardWiredOn $testFire) laser=$(Test-HazardWiredOn $testLaser) fan=$(Test-HazardWiredOn $testFan) fireOpacity=$($testFire.Element.Opacity) laserOpacity=$($testLaser.Element.Opacity) fanOpacity=$($testFan.Art.Opacity)")
$testInputs[0].SignalOn=$true;$testInputs[1].PulseTicks=30
New-PlayroomWire $testInputs[0] $testHazards[0];New-PlayroomWire $testInputs[1] $testHazards[1];New-PlayroomWire $testInputs[3] $testHazards[2];New-PlayroomWire $testInputs[0] $testFan
Set-WireChannel $script:playroomWires[0] 5
Update-WiringSignals
[Console]::Out.WriteLine("diagnostic:wiring wires=$($script:playroomWires.Count) spike=$(Test-HazardWiredOn $testHazards[0]) fire=$(Test-HazardWiredOn $testHazards[1]) laser=$(Test-HazardWiredOn $testHazards[2]) fan=$(Test-HazardWiredOn $testFan) channels=$(@($script:playroomWires|ForEach-Object{$_.Channel})-join',')")
$testInputs[0].SignalOn=$false;$testInputs[1].PulseTicks=0;Update-WiringSignals
[Console]::Out.WriteLine("diagnostic:wiring-off spike=$(Test-HazardWiredOn $testHazards[0]) fire=$(Test-HazardWiredOn $testHazards[1])")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_ROPE_BLOCK_COLLISION-eq'1'){
$testAnchor=@($script:playroomBlocks|Select-Object -First 1)[0]
$testRoutedBall=@($script:playroomBalls|Select-Object -First 1)[0]
Add-PlayroomBlock 'Normal'
$testObstacle=@($script:playroomBlocks|Select-Object -Last 1)[0]
if($null-ne$testAnchor-and$null-ne$testRoutedBall-and$null-ne$testObstacle){
$testLeft=[Windows.SystemParameters]::VirtualScreenLeft;$testTop=[Windows.SystemParameters]::VirtualScreenTop
$testAnchor.X=$testLeft+220;$testAnchor.Y=$testTop+180;Set-PlacementWindowPosition $testAnchor
$testObstacle.X=$testLeft+400;$testObstacle.Y=$testTop+300;Set-PlacementAngle $testObstacle 25;Set-PlacementWindowPosition $testObstacle
$testRoutedBall.X=$testLeft+600;$testRoutedBall.Y=$testTop+220;$testRoutedBall.VX=0;$testRoutedBall.VY=0;Set-PlacementWindowPosition $testRoutedBall
New-PlayroomRope $testAnchor $testRoutedBall 460
[Console]::Out.WriteLine('diagnostic:rope-block-test-started')
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_FREE_ROTATE-eq'1'){
$testFreeRotate=@($script:playroomBlocks|Select-Object -First 1)[0]
if($null-ne$testFreeRotate){
Show-FreeRotationHandle $testFreeRotate
Set-PlacementAngle $testFreeRotate 37.4
[Console]::Out.WriteLine("diagnostic:free-rotate angle=$($testFreeRotate.Angle) handleVisible=$($script:activeRotationHandleWindow.IsVisible)")
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_FROZEN_ROPE-eq'1'){
Add-PlayroomBlock 'Ice'
$testIce=@($script:playroomBlocks|Where-Object{$_.Type-eq'Ice'}|Select-Object -Last 1)[0]
$testFrozenBall=@($script:playroomBalls|Select-Object -First 1)[0]
if($null-ne$testIce-and$null-ne$testFrozenBall){
$testFrozenLeft=[Windows.SystemParameters]::VirtualScreenLeft;$testFrozenTop=[Windows.SystemParameters]::VirtualScreenTop
$testIce.X=$testFrozenLeft+420;$testIce.Y=$testFrozenTop+360;Set-PlacementWindowPosition $testIce
$testFrozenBall.X=$testFrozenLeft+680;$testFrozenBall.Y=$testFrozenTop+360;Set-PlacementWindowPosition $testFrozenBall
New-PlayroomRope $testIce $testFrozenBall
$testFrozenRope=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'}|Select-Object -Last 1)[0]
$beforeFrozenX=$testFrozenBall.X;$beforeFrozenY=$testFrozenBall.Y
$physicsOwned=$null-ne$testFrozenBall.PSObject.Properties['FrozenRope']-and$testFrozenBall.FrozenRope-eq$testFrozenRope
$initialFrozenRadius=$testFrozenRope.FrozenRadius
$testFrozenRope.DragStartLength=$testFrozenRope.Length
$testFrozenRope.DragStartFrozenRadius=$initialFrozenRadius
$testFrozenRope.DragStartX=$testFrozenRope.Window.Left+13;$testFrozenRope.DragStartY=$testFrozenRope.Window.Top+13
$testFrozenRope.Dragging=$true;$testFrozenRope.Window.Top-=55
Set-RopeLengthFromHandle $testFrozenRope
$testFrozenRope.Dragging=$false;Update-OnePlayroomRope $testFrozenRope $true
$resizedFrozenRadius=$testFrozenRope.FrozenRadius
Set-PlacementAngle $testIce 90
Update-FrozenRope $testFrozenRope
[Console]::Out.WriteLine("diagnostic:frozen-rope frozen=$($testFrozenRope.Frozen) physicsOwned=$physicsOwned initialRadius=$initialFrozenRadius resizedRadius=$resizedFrozenRadius beforeX=$beforeFrozenX beforeY=$beforeFrozenY afterX=$($testFrozenBall.X) afterY=$($testFrozenBall.Y) iceAngle=$($testIce.Angle)")
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_FROZEN_ROPE_DEVICES-eq'1'){
$stressLeft=[Windows.SystemParameters]::VirtualScreenLeft;$stressTop=[Windows.SystemParameters]::VirtualScreenTop
$stressPairs=New-Object Collections.ArrayList
$stressKinds=@('FireJet','Laser','Spikes')
for($stressIndex=0;$stressIndex-lt$stressKinds.Count;$stressIndex++){
Add-PlayroomBlock 'Ice';Add-PlayroomHazard $stressKinds[$stressIndex]
$stressIce=@($script:playroomBlocks|Where-Object{$_.Type-eq'Ice'}|Select-Object -Last 1)[0]
$stressDevice=@($script:playroomGadgets|Where-Object{$_.Kind-eq$stressKinds[$stressIndex]}|Select-Object -Last 1)[0]
$stressIce.X=$stressLeft+180+($stressIndex*330);$stressIce.Y=$stressTop+310;Set-PlacementWindowPosition $stressIce
$stressDevice.X=$stressIce.X+185;$stressDevice.Y=$stressIce.Y-95;Set-PlacementWindowPosition $stressDevice
if($stressDevice.Kind-eq'FireJet'){$stressDevice.ManualPower=$true}
New-PlayroomRope $stressIce $stressDevice
$stressPairs.Add([pscustomobject]@{Anchor=$stressIce;Device=$stressDevice;Rope=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'}|Select-Object -Last 1)[0]})|Out-Null
}
Add-PlayroomGadget 'Rotator'
$stressRotator=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Rotator'}|Select-Object -Last 1)[0]
$stressRotator.Target=$stressPairs[0].Anchor;$stressRotator.RotationSpeed=2.0
$fireInactiveTicks=0
$stressClock=[Diagnostics.Stopwatch]::StartNew()
for($stressTick=0;$stressTick-lt240;$stressTick++){
Update-PlayroomGadgets;Update-PlayroomHazards
if(-not$stressPairs[0].Device.Active){$fireInactiveTicks++}
}
$stressClock.Stop()
$maxRebaseError=0.0;$allFinite=$true
foreach($pair in @($stressPairs)){
$pair.Device.X+=37;$pair.Device.Y-=23;Set-PlacementWindowPosition $pair.Device
Rebase-FrozenRopeForPlacement $pair.Device
$expected=Get-RopeAttachmentPoint $pair.Device
Update-FrozenRope $pair.Rope
$actual=Get-RopeAttachmentPoint $pair.Device
$error=[Math]::Sqrt((($actual.X-$expected.X)*($actual.X-$expected.X))+(($actual.Y-$expected.Y)*($actual.Y-$expected.Y)))
$maxRebaseError=[Math]::Max($maxRebaseError,$error)
if([double]::IsNaN($pair.Device.X)-or[double]::IsInfinity($pair.Device.X)-or[double]::IsNaN($pair.Device.Y)-or[double]::IsInfinity($pair.Device.Y)){$allFinite=$false}
}
[Console]::Out.WriteLine("diagnostic:frozen-device-stress pairs=$($stressPairs.Count) ticks=240 elapsedMs=$($stressClock.ElapsedMilliseconds) finite=$allFinite maxRebaseError=$([Math]::Round($maxRebaseError,4)) fireInactiveTicks=$fireInactiveTicks rotatorAngle=$([Math]::Round($stressPairs[0].Anchor.Angle,1))")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_SEAMLESS_BLOCKS-eq'1'){
$testSeamlessBlock=@($script:playroomBlocks|Select-Object -First 1)[0]
if($null-ne$testSeamlessBlock){
$testSeamlessBlock.Width=560;$testSeamlessBlock.Height=230
$testSeamlessBlock.X=[Windows.SystemParameters]::VirtualScreenLeft+(([Windows.SystemParameters]::VirtualScreenWidth-$testSeamlessBlock.Width)/2)
$testSeamlessBlock.Y=[Windows.SystemParameters]::VirtualScreenTop+(([Windows.SystemParameters]::VirtualScreenHeight-$testSeamlessBlock.Height)/2)
$testSeamlessBlock.Window.Width=$testSeamlessBlock.Width;$testSeamlessBlock.Window.Height=$testSeamlessBlock.Height
Set-PlacementWindowPosition $testSeamlessBlock
[Console]::Out.WriteLine('diagnostic:seamless-block-resized')
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_FIREJET_VISUAL-eq'1'){
Add-PlayroomHazard 'FireJet'
[Console]::Out.WriteLine('diagnostic:firejet-visual-spawned')
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_HAZARD_DRAG-eq'1'){
Add-PlayroomHazard 'FireJet'
$testDragHazard=@($script:playroomGadgets|Where-Object{$_.Kind-eq'FireJet'}|Select-Object -Last 1)[0]
$testDragHazard.X=[Windows.SystemParameters]::VirtualScreenLeft+180;$testDragHazard.Y=[Windows.SystemParameters]::VirtualScreenTop+160;$testDragHazard.VY=9;$testDragHazard.Dragging=$true
$heldY=$testDragHazard.Y;Update-DynamicHazards;$heldAfter=$testDragHazard.Y
$testDragHazard.Dragging=$false;Update-DynamicHazards
[Console]::Out.WriteLine("diagnostic:hazard-drag heldY=$heldY heldAfter=$heldAfter releasedY=$($testDragHazard.Y) releasedVY=$($testDragHazard.VY)")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_LASER_VISUAL-eq'1'){
Add-PlayroomHazard 'Laser'
$testLaser=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Laser'}|Select-Object -Last 1)[0]
if($null-ne$testLaser){
$script:hearts=1
$script:x=$testLaser.X+205
$script:y=$testLaser.Y+150-($PetHeight/2)
$script:vx=0;$script:vy=0
}
[Console]::Out.WriteLine('diagnostic:laser-visual-spawned')
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_LASER_SUPPORT-eq'1'){
if(-not$script:playroomActive){Start-Playroom}
$supportBlock=@($script:playroomBlocks|Where-Object{$_.Type-eq'Normal'}|Select-Object -First 1)[0]
if($null-ne$supportBlock){
$supportBlock.Width=360;$supportBlock.Height=42
$supportBlock.X=[Windows.SystemParameters]::VirtualScreenLeft+(([Windows.SystemParameters]::VirtualScreenWidth-$supportBlock.Width)/2)
$supportBlock.Y=[Windows.SystemParameters]::VirtualScreenTop+460
Set-PlacementAngle $supportBlock 0
Add-PlayroomHazard 'Laser'
$supportLaser=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Laser'}|Select-Object -Last 1)[0]
$supportLaser.X=$supportBlock.X+(($supportBlock.Width-$supportLaser.BodyWidth)/2)-$supportLaser.BodyOffsetX
$supportLaser.Y=$supportBlock.Y-150-$supportLaser.BodyOffsetY-$supportLaser.BodyHeight
$supportLaser.VX=0;$supportLaser.VY=0;Set-PlacementWindowPosition $supportLaser
$script:diagnosticSupportCases=@([pscustomobject]@{Name='Laser/Normal';Object=$supportLaser;Block=$supportBlock;Ticks=0;Complete=$false})
[Console]::Out.WriteLine("diagnostic:support-start case=Laser/Normal bodyBottom=$($supportLaser.Y+$supportLaser.BodyOffsetY+$supportLaser.BodyHeight) blockTop=$($supportBlock.Y)")
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_BAT_SWING-eq'1'){
Add-PlayroomToy 'Bat'
$testBat=@($script:playroomBalls|Where-Object{$_.Kind-eq'Bat'}|Select-Object -Last 1)[0]
$testBall=@($script:playroomBalls|Where-Object{$_.Kind-eq'Ball'}|Select-Object -First 1)[0]
if($null-ne$testBat-and$null-ne$testBall){
$script:facing=1;$testBat.CarriedBy='Player1'
$testBall.X=$script:x+($PetWidth/2)+70-$testBall.Radius
$testBall.Y=$script:y+46-$testBall.Radius
$testBall.VX=0;$testBall.VY=0;$testBall.PetHitCooldown=99
Start-BatCharge 'Player1'|Out-Null;$testBat.BatChargeTicks=75;Release-BatSwing 'Player1'|Out-Null
[Console]::Out.WriteLine("diagnostic:bat-swing-started power=$($testBat.SwingPower)")
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_BONE-eq'1'){
Add-PlayroomToy 'Bone'
$testBone=@($script:playroomBalls|Where-Object{$_.Kind-eq'Bone'}|Select-Object -Last 1)[0]
if($null-ne$testBone){
$testBone.CarriedBy='Player1'
Set-PlayroomObjectVisual $testBone 24
[Console]::Out.WriteLine("diagnostic:bone-spawned visualScaleNull=$($null-eq$testBone.VisualScale) carried=$($testBone.CarriedBy)")
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_EMPTY_ATTACK-eq'1'){
$emptyAttackResult=Start-BatCharge 'Player1'
[Console]::Out.WriteLine("diagnostic:empty-attack-safe result=$emptyAttackResult")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_SPRING_DIRECTION-eq'1'){
Add-PlayroomGadget 'Spring'
$testSpring=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Spring'}|Select-Object -Last 1)[0]
$testBall=@($script:playroomBalls|Where-Object{$_.Kind-eq'Ball'}|Select-Object -First 1)[0]
if($null-ne$testSpring-and$null-ne$testBall){
Set-PlacementAngle $testSpring 90
$normalX=1.0;$normalY=0.0
$centreX=$testSpring.X+$testSpring.Width/2+($normalX*($testSpring.Height/2+$testBall.Radius-2))
$centreY=$testSpring.Y+$testSpring.Height/2
$testBall.X=$centreX-$testBall.Radius;$testBall.Y=$centreY-$testBall.Radius
$testBall.VX=-2;$testBall.VY=0;$testBall.PetHitCooldown=99
[Console]::Out.WriteLine('diagnostic:spring-direction-test-started')
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_SPRING_HAZARD-eq'1'){
Add-PlayroomGadget 'Spring'
Add-PlayroomHazard 'Laser'
$testSpring=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Spring'}|Select-Object -Last 1)[0]
$testLaser=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Laser'}|Select-Object -Last 1)[0]
if($null-ne$testSpring-and$null-ne$testLaser){
$testSpring.X=[Windows.SystemParameters]::VirtualScreenLeft+240
$testSpring.Y=[Windows.SystemParameters]::VirtualScreenTop+[Windows.SystemParameters]::VirtualScreenHeight-210
Set-PlacementWindowPosition $testSpring
Set-PlacementAngle $testSpring 0
$testRadius=[Math]::Min($testLaser.BodyWidth,$testLaser.BodyHeight)*.46
$testCentreX=$testSpring.X+($testSpring.Width/2)
$testCentreY=$testSpring.Y-$testRadius+2
$testLaser.X=$testCentreX-$testLaser.BodyOffsetX-($testLaser.BodyWidth/2)
$testLaser.Y=$testCentreY-$testLaser.BodyOffsetY-($testLaser.BodyHeight/2)
$testLaser.VX=0;$testLaser.VY=3
Set-PlacementWindowPosition $testLaser
[Console]::Out.WriteLine('diagnostic:spring-hazard-test-started')
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_SPRING_ALL-eq'1'){
Set-TwoPlayerMode $true;Set-Player2Controller 'Human'
Add-PlayroomGadget 'Spring'
Add-PlayroomToy 'Bat';Add-PlayroomToy 'Bone'
Add-PlayroomGadget 'Fan';Add-PlayroomGadget 'Teleporter'
foreach($kind in @('Spikes','FireJet','Laser')){Add-PlayroomHazard $kind}
$testSpring=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Spring'}|Select-Object -Last 1)[0]
if($null-ne$testSpring){
$testSpring.X=[Windows.SystemParameters]::VirtualScreenLeft+([Windows.SystemParameters]::VirtualScreenWidth-$testSpring.Width)/2
$testSpring.Y=[Windows.SystemParameters]::VirtualScreenTop+[Windows.SystemParameters]::VirtualScreenHeight-220
Set-PlacementAngle $testSpring 0;Set-PlacementWindowPosition $testSpring
$springCentreX=$testSpring.X+$testSpring.Width/2
$playerRadius=[Math]::Min($PetWidth,$PetHeight)*.38
$playerCentreY=$testSpring.Y-$playerRadius+2
$script:x=$springCentreX-($PetWidth/2);$script:y=$playerCentreY-($PetHeight/2);$script:vx=0;$script:vy=4;$script:grounded=$false
$script:p2X=$script:x+4;$script:p2Y=$script:y;$script:p2VX=0;$script:p2VY=4;$script:p2Grounded=$false
foreach($toy in @($script:playroomBalls)){
$toy.X=$springCentreX-$toy.Radius;$toy.Y=$testSpring.Y-$toy.Radius+2-$toy.Radius
$toy.VX=0;$toy.VY=4;$toy.CarriedBy='';$toy.PetHitCooldown=99
$toy.Window.Left=$toy.X;$toy.Window.Top=$toy.Y
}
$dynamicTargets=@($script:playroomGadgets|Where-Object{$null-ne$_.PSObject.Properties['IsDynamic']-and$_.IsDynamic})
foreach($target in $dynamicTargets){
$radius=[Math]::Min($target.BodyWidth,$target.BodyHeight)*.46
$target.X=$springCentreX-$target.BodyOffsetX-($target.BodyWidth/2)
$target.Y=$testSpring.Y-$radius+2-$target.BodyOffsetY-($target.BodyHeight/2)
$target.VX=0;$target.VY=4;$target.SpringCooldown=0;Set-PlacementWindowPosition $target
if($target.Kind-eq'Fan'){$target.ManualPower=$false}
$probe=Get-SpringTopContact $testSpring ($target.X+$target.BodyOffsetX+$target.BodyWidth/2) ($target.Y+$target.BodyOffsetY+$target.BodyHeight/2) $radius $target.VX $target.VY
[Console]::Out.WriteLine("diagnostic:spring-all-probe target=$($target.Kind) contact=$($null-ne$probe) radius=$([Math]::Round($radius,2)) centre=$([Math]::Round($target.X+$target.BodyOffsetX+$target.BodyWidth/2,2)),$([Math]::Round($target.Y+$target.BodyOffsetY+$target.BodyHeight/2,2)) spring=$([Math]::Round($testSpring.X+$testSpring.Width/2,2)),$([Math]::Round($testSpring.Y+$testSpring.Height/2,2)) angle=$($testSpring.Angle)")
}
[Console]::Out.WriteLine("diagnostic:spring-all-start toys=$($script:playroomBalls.Count) dynamic=$($dynamicTargets.Count) targets=$($script:playroomBalls.Count+$dynamicTargets.Count+2)")
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_FAN_ALL-eq'1'){
Set-TwoPlayerMode $true;Set-Player2Controller 'Human'
Add-PlayroomGadget 'Fan';Add-PlayroomHazard 'Laser'
$testFan=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Fan'}|Select-Object -Last 1)[0]
$testLaser=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Laser'}|Select-Object -Last 1)[0]
$testBall=@($script:playroomBalls|Where-Object{$_.Kind-eq'Ball'}|Select-Object -First 1)[0]
if($null-ne$testFan-and$null-ne$testLaser-and$null-ne$testBall){
$testFan.X=[Windows.SystemParameters]::VirtualScreenLeft+280;$testFan.Y=[Windows.SystemParameters]::VirtualScreenTop+300
$testFan.VX=0;$testFan.VY=0;$testFan.Dragging=$true;$testFan.ManualPower=$true;Set-PlacementAngle $testFan 0;Set-PlacementWindowPosition $testFan
$originX=$testFan.X+$testFan.Width/2;$originY=$testFan.Y+$testFan.Height/2
$script:x=$originX+120-($PetWidth/2);$script:y=$originY-($PetHeight/2);$script:vx=0;$script:vy=0
$script:p2X=$script:x;$script:p2Y=$script:y+20;$script:p2VX=0;$script:p2VY=0
$testBall.X=$originX+120-$testBall.Radius;$testBall.Y=$originY-$testBall.Radius;$testBall.VX=0;$testBall.VY=0
$testLaser.X=$originX+120-$testLaser.BodyOffsetX-($testLaser.BodyWidth/2);$testLaser.Y=$originY-$testLaser.BodyOffsetY-($testLaser.BodyHeight/2);$testLaser.VX=0;$testLaser.VY=0;Set-PlacementWindowPosition $testLaser
Update-PlayroomGadgets
$horizontalPass=$script:vx-gt0-and$script:p2VX-gt0-and$testBall.VX-gt0-and$testLaser.VX-gt0-and$testFan.VX-eq0-and$testFan.VY-eq0
[Console]::Out.WriteLine("diagnostic:fan-all angle=0 pass=$horizontalPass p1=$([Math]::Round($script:vx,2)) p2=$([Math]::Round($script:p2VX,2)) ball=$([Math]::Round($testBall.VX,2)) laser=$([Math]::Round($testLaser.VX,2)) fan=$([Math]::Round($testFan.VX,2)),$([Math]::Round($testFan.VY,2))")
$testFan.ManualPower=$false;$script:vx=0;$script:p2VX=0;$testBall.VX=0;$testLaser.VX=0;Update-PlayroomGadgets
$offPass=$script:vx-eq0-and$script:p2VX-eq0-and$testBall.VX-eq0-and$testLaser.VX-eq0-and@($testFan.WindLines|Where-Object{$_.Line.Opacity-gt0}).Count-eq0
[Console]::Out.WriteLine("diagnostic:fan-all poweredOff pass=$offPass visibleWind=$(@($testFan.WindLines|Where-Object{$_.Line.Opacity-gt0}).Count)")
$testFan.ManualPower=$true;Set-PlacementAngle $testFan 90
$script:x=$originX-($PetWidth/2);$script:y=$originY+120-($PetHeight/2);$script:vx=0;$script:vy=0
$testBall.X=$originX-$testBall.Radius;$testBall.Y=$originY+120-$testBall.Radius;$testBall.VX=0;$testBall.VY=0
Update-PlayroomGadgets
$verticalPass=[Math]::Abs($script:vx)-lt.01-and$script:vy-gt0-and[Math]::Abs($testBall.VX)-lt.01-and$testBall.VY-gt0
[Console]::Out.WriteLine("diagnostic:fan-all angle=90 pass=$verticalPass p1=$([Math]::Round($script:vx,2)),$([Math]::Round($script:vy,2)) ball=$([Math]::Round($testBall.VX,2)),$([Math]::Round($testBall.VY,2))")
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_GADGET_LOAD-in@('Baseline','Fans','Ropes3','Ropes','BlockBaseline','BlocksSeparated','BlocksLayered','LaddersSeparated','LaddersLayered','LaddersSeparatedMoving','LaddersLayeredMoving')){
$script:gadgetBenchmarkPhase=$env:WINDOWISP_TEST_GADGET_LOAD
while($script:playroomBalls.Count-lt5){Add-PlayroomBall}
$benchLeft=[Windows.SystemParameters]::VirtualScreenLeft;$benchTop=[Windows.SystemParameters]::VirtualScreenTop
$benchBalls=@($script:playroomBalls|Select-Object -First 5)
for($i=0;$i-lt$benchBalls.Count;$i++){
$benchBalls[$i].X=$benchLeft+430+($i*45);$benchBalls[$i].Y=$benchTop+180+($i*105)
$benchBalls[$i].VX=1.2;$benchBalls[$i].VY=0;$benchBalls[$i].Window.Left=$benchBalls[$i].X;$benchBalls[$i].Window.Top=$benchBalls[$i].Y
}
if($script:gadgetBenchmarkPhase-eq'Fans'){
for($i=0;$i-lt5;$i++){
Add-PlayroomGadget 'Fan';$benchFan=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Fan'}|Select-Object -Last 1)[0]
$benchFan.X=$benchLeft+270;$benchFan.Y=$benchTop+165+($i*105);$benchFan.VX=0;$benchFan.VY=0;$benchFan.Dragging=$true;$benchFan.ManualPower=$true
Set-PlacementAngle $benchFan 0;Set-PlacementWindowPosition $benchFan
}
}elseif($script:gadgetBenchmarkPhase-in@('Ropes3','Ropes')){
$benchAnchor=@($script:playroomBlocks|Select-Object -First 1)[0]
$benchAnchor.X=$benchLeft+245;$benchAnchor.Y=$benchTop+130;$benchAnchor.Width=200;$benchAnchor.Height=32
$benchAnchor.Window.Width=$benchAnchor.Width;$benchAnchor.Window.Height=$benchAnchor.Height;Set-PlacementWindowPosition $benchAnchor
$benchRopeCount=$(if($script:gadgetBenchmarkPhase-eq'Ropes3'){3}else{5})
for($i=0;$i-lt$benchRopeCount;$i++){
$benchBalls[$i].X=$benchLeft+500+($i*55);$benchBalls[$i].Y=$benchTop+260+($i*75)
$benchBalls[$i].Window.Left=$benchBalls[$i].X;$benchBalls[$i].Window.Top=$benchBalls[$i].Y
New-PlayroomRope $benchAnchor $benchBalls[$i] 220
}
}elseif($script:gadgetBenchmarkPhase-in@('BlockBaseline','BlocksSeparated','BlocksLayered','LaddersSeparated','LaddersLayered','LaddersSeparatedMoving','LaddersLayeredMoving')){
Set-TwoPlayerMode $true;Set-Player2Controller 'Human'
if($script:gadgetBenchmarkPhase-in@('BlocksSeparated','BlocksLayered')){
while($script:playroomBlocks.Count-lt5){Add-PlayroomBlock 'Normal'}
$benchBlocks=@($script:playroomBlocks|Select-Object -First 5)
for($i=0;$i-lt$benchBlocks.Count;$i++){
$benchBlocks[$i].X=$(if($script:gadgetBenchmarkPhase-eq'BlocksLayered'){$benchLeft+520}else{$benchLeft+180+($i*210)})
$benchBlocks[$i].Y=$(if($script:gadgetBenchmarkPhase-eq'BlocksLayered'){$benchTop+460}else{$benchTop+260+(($i%2)*230)})
Set-PlacementAngle $benchBlocks[$i] 0;Set-PlacementWindowPosition $benchBlocks[$i]
}
$contactBlock=$benchBlocks[0]
$script:x=$contactBlock.X+20;$script:y=$contactBlock.Y-$PetHeight;$script:vx=1.5;$script:vy=2
$script:p2X=$contactBlock.X+$contactBlock.Width-$PetWidth-20;$script:p2Y=$script:y;$script:p2VX=-1.5;$script:p2VY=2
foreach($ball in $benchBalls){$ball.X=$contactBlock.X+($contactBlock.Width-$ball.Size)/2;$ball.Y=$contactBlock.Y-$ball.Size-20;$ball.VX=1.1;$ball.VY=2;$ball.Window.Left=$ball.X;$ball.Window.Top=$ball.Y}
}elseif($script:gadgetBenchmarkPhase-like'Ladders*'){
Add-PlayroomBlock 'Ladder';Add-PlayroomBlock 'Ladder'
$benchLadders=@($script:playroomBlocks|Where-Object{$_.Type-eq'Ladder'}|Select-Object -Last 2)
for($i=0;$i-lt$benchLadders.Count;$i++){
$benchLadders[$i].X=$(if($script:gadgetBenchmarkPhase-like'LaddersLayered*'){$benchLeft+570}else{$benchLeft+420+($i*300)})
$benchLadders[$i].Y=$benchTop+330;Set-PlacementAngle $benchLadders[$i] 0;Set-PlacementWindowPosition $benchLadders[$i]
$benchLadders[$i]|Add-Member -NotePropertyName BenchmarkBaseX -NotePropertyValue ([double]$benchLadders[$i].X) -Force
$benchLadders[$i]|Add-Member -NotePropertyName BenchmarkBaseY -NotePropertyValue ([double]$benchLadders[$i].Y) -Force
}
$script:x=$benchLadders[0].X+($benchLadders[0].Width-$PetWidth)/2;$script:y=$benchLadders[0].Y+30;$script:vx=0;$script:vy=0
$script:p2X=$benchLadders[$benchLadders.Count-1].X+($benchLadders[$benchLadders.Count-1].Width-$PetWidth)/2;$script:p2Y=$benchLadders[$benchLadders.Count-1].Y+45;$script:p2VX=0;$script:p2VY=0
}
}
$benchProcess=[Diagnostics.Process]::GetCurrentProcess();$benchProcess.Refresh()
$script:gadgetBenchmarkCpuStartMs=$benchProcess.TotalProcessorTime.TotalMilliseconds
$script:gadgetBenchmarkWallStartMs=$script:frameClock.Elapsed.TotalMilliseconds
$script:gadgetBenchmarkStartHandles=$benchProcess.HandleCount
$script:gadgetBenchmarkStartWindows=0
$script:frameGapMax=0;$script:frameGapSum=0;$script:frameGapSamples=0;$script:lastFrameMs=$script:gadgetBenchmarkWallStartMs
[Console]::Out.WriteLine("diagnostic:gadget-benchmark-start phase=$($script:gadgetBenchmarkPhase) balls=$($script:playroomBalls.Count) blocks=$($script:playroomBlocks.Count) ladders=$(@($script:playroomBlocks|Where-Object{$_.Type-eq'Ladder'}).Count) fans=$(@($script:playroomGadgets|Where-Object{$_.Kind-eq'Fan'}).Count) ropes=$(@($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'}).Count) windows=$($script:gadgetBenchmarkStartWindows) handles=$($script:gadgetBenchmarkStartHandles)")
}
if($env:WINDOWISP_TEST_ROTATOR-eq'1'){
Add-PlayroomGadget 'Rotator'
$testRotator=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Rotator'}|Select-Object -Last 1)[0]
$testBlock=@($script:playroomBlocks|Select-Object -First 1)[0]
if($null-ne$testRotator-and$null-ne$testBlock){
$desiredRotatorX=$testBlock.X+20;$desiredRotatorY=$testBlock.Y+35
$testRotator.X=$desiredRotatorX;$testRotator.Y=$desiredRotatorY
$testRotator.Window.Left=$desiredRotatorX;$testRotator.Window.Top=$desiredRotatorY
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine('diagnostic:rotator-spawned')}
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_BALL_INTERACT -eq '1' -and $script:playroomBalls.Count -gt 0) {
$testBall = $script:playroomBalls[0]
$testBall.X = $script:x + $PetWidth - 12; $testBall.Y = $script:y + 24
$testBall.VX = 0; $testBall.VY = 0
$testBall.CarriedBy = 'Player1'
$testBall.Window.Left = $testBall.X; $testBall.Window.Top = $testBall.Y
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_BALL_BLOCK_SIDE -eq '1' -and
$script:playroomBalls.Count -gt 0 -and $script:playroomBlocks.Count -gt 0) {
$testBall = $script:playroomBalls[0]; $testBlock = $script:playroomBlocks[0]
$testBall.X = $testBlock.X - $testBall.Size - 24
$testBall.Y = $testBlock.Y - (($testBall.Size - $testBlock.Height) / 2)
$testBall.VX = 8; $testBall.VY = 0; $testBall.PetHitCooldown = 999
$testBall.Window.Left = $testBall.X; $testBall.Window.Top = $testBall.Y
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_BLOCK_ART -eq '1') {
$artTypes=@('Normal','Wooden','Bouncy','Fire','Ice','Grass','Mirror','Conveyor','Cloud','Crumbling')
for($artIndex=0;$artIndex-lt$artTypes.Count;$artIndex++){
Add-PlayroomBlock $artTypes[$artIndex]
$artBlock=@($script:playroomBlocks|Where-Object{$_.Type-eq$artTypes[$artIndex]}|Select-Object -Last 1)[0]
$artColumn=$artIndex%5;$artRow=[Math]::Floor($artIndex/5)
$artBlock.Width=220;$artBlock.Height=$(if($artBlock.Type-in@('Wooden','Conveyor')){$artBlock.Height}else{220})
$artBlock.X=[Windows.SystemParameters]::VirtualScreenLeft+32+($artColumn*260);$artBlock.Y=[Windows.SystemParameters]::VirtualScreenTop+155+($artRow*290)
Set-PlacementAngle $artBlock 0
}
[Console]::Out.WriteLine("diagnostic:block-art types=$($artTypes-join',') fixedWood=$(@($script:playroomBlocks|Where-Object{$_.Type-eq'Wooden'}|Select-Object -Last 1)[0].Height) fixedConveyor=$(@($script:playroomBlocks|Where-Object{$_.Type-eq'Conveyor'}|Select-Object -Last 1)[0].Height)")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_CRUMBLE_ANIMATION-eq'1'){
Add-PlayroomBlock 'Crumbling';$script:crumbleTestBlock=@($script:playroomBlocks|Select-Object -Last 1)[0]
$script:crumbleTestBlock.Width=460;$script:crumbleTestBlock.Height=120
$script:crumbleTestBlock.X=[Windows.SystemParameters]::VirtualScreenLeft+(([Windows.SystemParameters]::VirtualScreenWidth-460)/2)
$script:crumbleTestBlock.Y=[Windows.SystemParameters]::VirtualScreenTop+330;Set-PlacementAngle $script:crumbleTestBlock 0
$script:crumbleTestStartTick=$script:hazardTick
[Console]::Out.WriteLine('diagnostic:crumble-animation-ready impacts=0')
}
Update-PlatformHighlights
Update-Menu
}
function Stop-Playroom {
if (-not $script:playroomActive) { return }
if($script:wispfallActive-or$null-ne$script:wispfallLavaWindow){Remove-WispfallSetup}
Set-GridSnap $false
Set-EraserMode $false
Close-FreeRotationHandle
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine('diagnostic:stop-playroom-begin')}
Clear-PlayroomBalls
Set-SandboxToolboxVisible $false
$script:playroomActive = $false
Set-WireEditingVisible $false
Set-RopeEditingVisible $false
foreach ($blockData in @($script:playroomBlocks)) { Set-BlockGameplayVisual $blockData $true }
foreach($placement in @($script:playroomGadgets)){
if($placement.Kind-in@('Rotator','Rope')){$placement.Window.Hide()}
else{Set-OverlayClickThroughState $placement.Window $true}
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output 'diagnostic:desktop-play-stop' }
$script:enemySpawnTicks = 120; $script:starSpawnTicks = 90; $script:shieldPickupSpawnTicks = 45
Update-PlatformHighlights; Update-Menu
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine('diagnostic:stop-playroom-end')}
}
function Clear-Dodgeballs {
foreach ($ball in @($script:dodgeballs)) {
Remove-SharedGameplayElement $ball.Element
}
$script:dodgeballs.Clear()
}
function New-Dodgeball {
$size = 46.0
$fromRight = $script:dodgeballFromRight
$script:dodgeballFromRight = -not $script:dodgeballFromRight
$left = [Windows.SystemParameters]::VirtualScreenLeft
$top = [Windows.SystemParameters]::VirtualScreenTop
$width = [Windows.SystemParameters]::VirtualScreenWidth
$ballWindow = New-Object Windows.Window
$ballWindow.Title = 'Windowisp Dodgeball'
$ballWindow.Width = $size; $ballWindow.Height = $size
$ballWindow.WindowStyle = [Windows.WindowStyle]::None
$ballWindow.ResizeMode = [Windows.ResizeMode]::NoResize
$ballWindow.AllowsTransparency = $true
$ballWindow.Background = [Windows.Media.Brushes]::Transparent
$ballWindow.ShowInTaskbar = $false; $ballWindow.ShowActivated=$false
$ballWindow.Focusable=$false;$ballWindow.Topmost = $script:alwaysOnTop
$grid = New-Object Windows.Controls.Grid
$grid.Width=$size;$grid.Height=$size;$grid.IsHitTestVisible=$false
$ballArt=New-ToyboxImage 'Ball'
if($null-ne$ballArt){$ballArt.Width=$size;$ballArt.Height=$size;$grid.Children.Add($ballArt)|Out-Null}
$rotate = New-Object Windows.Media.RotateTransform
$rotate.CenterX = 23; $rotate.CenterY = 23; $grid.RenderTransform = $rotate
$x = $(if ($fromRight) { $left + $width - $size - 8 } else { $left + 8 })
$y = $top + 62
# Aim inward from either corner, then vary the launch by up to 33 degrees.
# The resulting velocity is never damped, so every ball keeps its own steady flight.
$baseAngleDegrees = 38.0
$launchAngleDegrees = $baseAngleDegrees + (($script:random.NextDouble() * 66.0) - 33.0)
$launchAngleDegrees = [Math]::Max(5.0, [Math]::Min(71.0, $launchAngleDegrees))
$launchRadians = $launchAngleDegrees * [Math]::PI / 180.0
$speed = 6.6
$vx = [Math]::Cos($launchRadians) * $speed * $(if ($fromRight) { -1 } else { 1 })
$vy = [Math]::Sin($launchRadians) * $speed
$ball = [pscustomobject]@{ Window=$null;Element=$grid; X=[double]$x; Y=[double]$y; VX=[double]$vx; VY=[double]$vy; Size=$size; Radius=($size/2); Rotate=$rotate; LaunchAngle=[double]$launchAngleDegrees }
Add-SharedGameplayElement $grid $x $y
$script:dodgeballs.Add($ball) | Out-Null
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output ('diagnostic:dodgeball-spawn side={0} angle={1:0.0} speed={2:0.0} count={3}' -f $(if($fromRight){'right'}else{'left'}),$launchAngleDegrees,$speed,$script:dodgeballs.Count) }
}
function Update-Dodgeballs {
$left = [Windows.SystemParameters]::VirtualScreenLeft
$top = [Windows.SystemParameters]::VirtualScreenTop
$right = $left + [Windows.SystemParameters]::VirtualScreenWidth
$bottom = $top + [Windows.SystemParameters]::VirtualScreenHeight
foreach ($ball in @($script:dodgeballs)) {
$oldX=$ball.X;$oldY=$ball.Y
$ball.X += $ball.VX; $ball.Y += $ball.VY
foreach($blockData in @($script:playroomBlocks)){
if([Math]::Abs($blockData.Angle)-gt.01){
$targetX=$ball.X;$targetY=$ball.Y
$travel=[Math]::Max([Math]::Abs($targetX-$oldX),[Math]::Abs($targetY-$oldY))
$sweepSteps=[Math]::Max(1,[Math]::Min(10,[Math]::Ceiling($travel/4.0)))
$rotatedHit=$false
for($sweepStep=1;$sweepStep-le$sweepSteps;$sweepStep++){
$sweepT=$sweepStep/[double]$sweepSteps
$ball.X=$oldX+(($targetX-$oldX)*$sweepT)
$ball.Y=$oldY+(($targetY-$oldY)*$sweepT)
if(Resolve-BallRotatedBlock $ball $blockData 1.0){$rotatedHit=$true;break}
}
if($rotatedHit){break}
$ball.X=$targetX;$ball.Y=$targetY
continue
}
$oldRight=$oldX+$ball.Size;$oldBottom=$oldY+$ball.Size
$newRight=$ball.X+$ball.Size;$newBottom=$ball.Y+$ball.Size
$horizontalOverlap=$newRight-3-gt$blockData.X -and $ball.X+3-lt$blockData.X+$blockData.Width
$verticalOverlap=$newBottom-3-gt$blockData.Y -and $ball.Y+3-lt$blockData.Y+$blockData.Height
if($ball.VY-gt0-and$horizontalOverlap-and$oldBottom-le$blockData.Y+4-and$newBottom-ge$blockData.Y){
$ball.Y=$blockData.Y-$ball.Size;$ball.VY=-[Math]::Abs($ball.VY);break
}elseif($ball.VY-lt0-and$horizontalOverlap-and$oldY-ge$blockData.Y+$blockData.Height-4-and$ball.Y-le$blockData.Y+$blockData.Height){
$ball.Y=$blockData.Y+$blockData.Height;$ball.VY=[Math]::Abs($ball.VY);break
}elseif($ball.VX-gt0-and$verticalOverlap-and$oldRight-le$blockData.X+4-and$newRight-ge$blockData.X){
$ball.X=$blockData.X-$ball.Size;$ball.VX=-[Math]::Abs($ball.VX);break
}elseif($ball.VX-lt0-and$verticalOverlap-and$oldX-ge$blockData.X+$blockData.Width-4-and$ball.X-le$blockData.X+$blockData.Width){
$ball.X=$blockData.X+$blockData.Width;$ball.VX=[Math]::Abs($ball.VX);break
}
}
if ($ball.X -le $left) { $ball.X = $left; $ball.VX = [Math]::Abs($ball.VX) }
elseif ($ball.X + $ball.Size -ge $right) { $ball.X = $right - $ball.Size; $ball.VX = -[Math]::Abs($ball.VX) }
if ($ball.Y -le $top) { $ball.Y = $top; $ball.VY = [Math]::Abs($ball.VY) }
elseif ($ball.Y + $ball.Size -ge $bottom) { $ball.Y = $bottom - $ball.Size; $ball.VY = -[Math]::Abs($ball.VY) }
# Physics remains 100 Hz, but transparent HWND composition is capped at
# 50 Hz. Updating both position and rotation every 10 ms made one ball
# enough to monopolise WPF's dispatcher on some desktops.
if(($script:inputTick%2)-eq0){
$ball.Rotate.Angle = ($ball.Rotate.Angle + ($(if($ball.VX -ge 0){10}else{-10}))) % 360
Set-SharedOverlayElementPosition $ball.Element $ball.X $ball.Y
}
$hitP1 = $script:invulnerableTicks -le 0 -and
($ball.X + $ball.Size -gt $script:x + 8) -and ($ball.X -lt $script:x + $PetWidth - 8) -and
($ball.Y + $ball.Size -gt $script:y + 8) -and ($ball.Y -lt $script:y + $PetHeight - 5)
$hitP2 = $script:twoPlayerActive -and $script:p2Hearts -gt 0 -and $script:p2InvulnerableTicks -le 0 -and
($ball.X + $ball.Size -gt $script:p2X + 8) -and ($ball.X -lt $script:p2X + $PetWidth - 8) -and
($ball.Y + $ball.Size -gt $script:p2Y + 8) -and ($ball.Y -lt $script:p2Y + $PetHeight - 5)
if ($hitP1 -or $hitP2) {
if ($hitP1) { $script:hearts--; $script:invulnerableTicks = 75 }
else { $script:p2Hearts--; $script:p2InvulnerableTicks = 75 }
Remove-SharedGameplayElement $ball.Element;$script:dodgeballs.Remove($ball)
if ($script:hearts -le 0) {
$script:active = $false; Register-ControlKeys $false; Set-ClickThrough $true; Update-Menu
}
}
}
}
function Update-HitProtectionVisuals {
$p1Protected = $script:active -and $script:invulnerableTicks -gt 0
$p1FlashOn = $p1Protected -and (([Math]::Floor($script:invulnerableTicks / 6) % 2) -eq 0)
$window.Opacity = $(if ($p1FlashOn) { 0.34 } else { 1.0 })
if ($null -ne $script:player2Window) {
$p2Protected = $script:active -and $script:p2InvulnerableTicks -gt 0
$p2FlashOn = $p2Protected -and (([Math]::Floor($script:p2InvulnerableTicks / 6) % 2) -eq 0)
$script:player2Window.Opacity = $(if ($p2FlashOn) { 0.34 } else { 1.0 })
}
}
function Remove-FootballSetup {
foreach($object in @($script:footballModeObjects|Where-Object{$null-ne$_})){
if($object.Kind-eq'Wire'){Remove-PlayroomPlacement $object}
elseif($object.Kind-eq'FootballDecoration'){try{$object.Window.Close()}catch{}}
}
foreach($object in @($script:footballModeObjects|Where-Object{$null-ne$_-and$_.Kind-notin@('Wire','FootballDecoration')})){
Remove-PlayroomPlacement $object
}
$script:footballModeObjects=@();$script:footballLeftGoal=$null;$script:footballRightGoal=$null
}
function New-FootballGoalFrame([string]$side,[double]$x,[double]$groundY) {
$goalWindow=New-Object Windows.Window;$goalWindow.Title="Windowisp $side Goal";$goalWindow.Width=132;$goalWindow.Height=126
$goalWindow.Left=$x;$goalWindow.Top=$groundY-$goalWindow.Height;$goalWindow.WindowStyle='None';$goalWindow.AllowsTransparency=$true
$goalWindow.Background=[Windows.Media.Brushes]::Transparent;$goalWindow.Topmost=$true;$goalWindow.ShowInTaskbar=$false;$goalWindow.ShowActivated=$false;$goalWindow.ResizeMode='NoResize'
$canvas=New-Object Windows.Controls.Canvas;$canvas.Width=132;$canvas.Height=126;$canvas.IsHitTestVisible=$false
$colour=New-Object Windows.Media.SolidColorBrush $(if($side-eq'P1'){[Windows.Media.Color]::FromRgb(73,181,255)}else{[Windows.Media.Color]::FromRgb(239,72,160)})
foreach($segment in @(@(8,120,8,20),@(8,20,124,20),@(124,20,124,120))){
$line=New-Object Windows.Shapes.Line;$line.X1=$segment[0];$line.Y1=$segment[1];$line.X2=$segment[2];$line.Y2=$segment[3]
$line.Stroke=$colour;$line.StrokeThickness=7;$line.StrokeStartLineCap='Round';$line.StrokeEndLineCap='Round';$canvas.Children.Add($line)|Out-Null
}
foreach($netX in @(32,56,80,104)){$line=New-Object Windows.Shapes.Line;$line.X1=$netX;$line.Y1=25;$line.X2=$netX;$line.Y2=116;$line.Stroke=$colour;$line.StrokeThickness=1;$line.Opacity=.35;$canvas.Children.Add($line)|Out-Null}
foreach($netY in @(44,66,88,110)){$line=New-Object Windows.Shapes.Line;$line.X1=12;$line.Y1=$netY;$line.X2=120;$line.Y2=$netY;$line.Stroke=$colour;$line.StrokeThickness=1;$line.Opacity=.35;$canvas.Children.Add($line)|Out-Null}
$label=New-Object Windows.Controls.TextBlock;$label.Text="$side GOAL";$label.Foreground=[Windows.Media.Brushes]::White;$label.FontWeight='Bold';$label.FontSize=12
[Windows.Controls.Canvas]::SetLeft($label,42);[Windows.Controls.Canvas]::SetTop($label,1);$canvas.Children.Add($label)|Out-Null
$goalWindow.Content=$canvas;$goalWindow.Show();Set-OverlayClickThrough $goalWindow
return [pscustomobject]@{Kind='FootballDecoration';Window=$goalWindow}
}
function Reset-FootballKickoff([string]$message='') {
if($null-eq$script:footballBall){return}
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$width=[Windows.SystemParameters]::VirtualScreenWidth
$script:footballBall.CarriedBy='';$script:footballBall.VX=0;$script:footballBall.VY=0
$script:footballBall.X=$left+($width-$script:footballBall.Size)/2
$script:footballBall.Y=$top+105
$script:footballBall.Window.Left=$script:footballBall.X;$script:footballBall.Window.Top=$script:footballBall.Y
$script:x=$left+($width*.22)-($PetWidth/2);$script:y=$top+120;$script:vx=0;$script:vy=0;$script:grounded=$false
$script:p2X=$left+($width*.78)-($PetWidth/2);$script:p2Y=$top+120;$script:p2VX=0;$script:p2VY=0;$script:p2Grounded=$false
if($null-ne$script:player2Window){$script:player2Window.Left=$script:p2X;$script:player2Window.Top=$script:p2Y}
$script:footballCountdownTicks=180;$script:footballKickoffTextTicks=45;$script:footballGoalCooldown=210
$script:footballMessage=$message
}
function Start-Football {
if(-not$script:playroomActive){Start-Playroom}
Remove-FootballSetup
Clear-PlayroomBalls
$script:footballActive=$true;$script:gameType='Football';$script:footballP1Score=0;$script:footballP2Score=0;$script:footballMatchOver=$false
$humanSecondPlayerAlreadyEnabled=$script:twoPlayerActive-and$script:p2ControllerMode-eq'Human'
Set-TwoPlayerMode $true
if(-not$humanSecondPlayerAlreadyEnabled-and$script:p2ControllerMode-eq'Human'){Set-Player2Controller 'Opponent'}
Add-PlayroomBall
$script:footballBall=@($script:playroomBalls|Where-Object{$_.Kind-eq'Ball'}|Select-Object -Last 1)[0]
$script:footballBall|Add-Member -NotePropertyName FootballBall -NotePropertyValue $true -Force
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$width=[Windows.SystemParameters]::VirtualScreenWidth;$height=[Windows.SystemParameters]::VirtualScreenHeight
Add-PlayroomGadget 'PressurePlate';$leftPlate=@($script:playroomGadgets|Where-Object{$_.Kind-eq'PressurePlate'}|Select-Object -Last 1)[0]
Add-PlayroomGadget 'PressurePlate';$rightPlate=@($script:playroomGadgets|Where-Object{$_.Kind-eq'PressurePlate'}|Select-Object -Last 1)[0]
Add-PlayroomHazard 'FireJet';$leftFire=@($script:playroomGadgets|Where-Object{$_.Kind-eq'FireJet'}|Select-Object -Last 1)[0]
Add-PlayroomHazard 'FireJet';$rightFire=@($script:playroomGadgets|Where-Object{$_.Kind-eq'FireJet'}|Select-Object -Last 1)[0]
# Keep the goal feet above the normal Windows taskbar without introducing
# a WinForms dependency into the WPF runtime.
$groundY=$top+$height-48
$leftPlate.X=$left+27;$leftPlate.Y=$groundY-$leftPlate.Height
$rightPlate.X=$left+$width-$rightPlate.Width-27;$rightPlate.Y=$groundY-$rightPlate.Height
$leftFire.X=$leftPlate.X+($leftPlate.Width-$leftFire.BodyWidth)/2-$leftFire.BodyOffsetX;$leftFire.Y=$leftPlate.Y-$leftFire.Height+10
$rightFire.X=$rightPlate.X+($rightPlate.Width-$rightFire.BodyWidth)/2-$rightFire.BodyOffsetX;$rightFire.Y=$rightPlate.Y-$rightFire.Height+10
foreach($placement in @($leftPlate,$rightPlate,$leftFire,$rightFire)){Set-PlacementWindowPosition $placement;$placement|Add-Member -NotePropertyName FootballGoal -NotePropertyValue $true -Force}
$leftFire.IsDynamic=$false;$rightFire.IsDynamic=$false
New-PlayroomWire $leftPlate $leftFire;$leftWire=@($script:playroomWires|Select-Object -Last 1)[0]
New-PlayroomWire $rightPlate $rightFire;$rightWire=@($script:playroomWires|Select-Object -Last 1)[0]
$leftFrame=New-FootballGoalFrame 'P2' ($left+8) $groundY
$rightFrame=New-FootballGoalFrame 'P1' ($left+$width-140) $groundY
$script:footballLeftGoal=$leftPlate;$script:footballRightGoal=$rightPlate
$script:footballModeObjects=@($leftWire,$rightWire,$leftPlate,$rightPlate,$leftFire,$rightFire,$leftFrame,$rightFrame)
Set-SandboxToolboxVisible $false;Set-WireEditingVisible $false;Set-RopeEditingVisible $false
foreach($block in @($script:playroomBlocks)){Set-BlockGameplayVisual $block $true}
foreach($placement in @($script:playroomGadgets)){Set-OverlayClickThroughState $placement.Window $true}
$script:active=$true;Register-ControlKeys $true;Set-ClickThrough $false
Reset-FootballKickoff
if($null-ne$script:gameModePanel){$script:gameModePanel.Visibility=[Windows.Visibility]::Collapsed}
if($null-ne$script:menuWindow-and$script:menuWindow.IsVisible){$script:menuWindow.Hide()}
Update-Hud;Update-Menu
}
function Update-Football {
if(-not$script:footballActive-or$null-eq$script:footballBall){return}
if($script:footballGoalCooldown-gt0){$script:footballGoalCooldown--}
if($script:footballCountdownTicks-gt0){
$script:footballCountdownTicks--
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop;$width=[Windows.SystemParameters]::VirtualScreenWidth
$script:footballBall.X=$left+($width-$script:footballBall.Size)/2;$script:footballBall.Y=$top+105
$script:footballBall.VX=0;$script:footballBall.VY=0;$script:footballBall.Window.Left=$script:footballBall.X;$script:footballBall.Window.Top=$script:footballBall.Y
return
}
if($script:footballKickoffTextTicks-gt0){$script:footballKickoffTextTicks--}
if($script:footballMatchOver-or$script:footballGoalCooldown-gt0){return}
$ball=$script:footballBall
$leftGoalHit=$ball.X+$ball.Size-gt$script:footballLeftGoal.X-and$ball.X-lt$script:footballLeftGoal.X+$script:footballLeftGoal.Width-and$ball.Y+$ball.Size-gt$script:footballLeftGoal.Y
$rightGoalHit=$ball.X+$ball.Size-gt$script:footballRightGoal.X-and$ball.X-lt$script:footballRightGoal.X+$script:footballRightGoal.Width-and$ball.Y+$ball.Size-gt$script:footballRightGoal.Y
if($leftGoalHit){$script:footballP2Score++;$message='GOAL! P2 SCORES'}elseif($rightGoalHit){$script:footballP1Score++;$message='GOAL! P1 SCORES'}else{return}
if($script:footballP1Score-ge3-or$script:footballP2Score-ge3){
$script:footballMatchOver=$true;$script:footballMessage=$(if($script:footballP1Score-gt$script:footballP2Score){'P1 WINS!'}else{'P2 WINS!'})
$ball.VX=0;$ball.VY=0
}else{Reset-FootballKickoff $message}
}
function Close-WispfallLava {
if($null-ne$script:wispfallLavaWindow){
try{$script:wispfallLavaWindow.Close()}catch{}
$script:wispfallLavaWindow=$null
}
}
function Hide-SandboxObjectsForWispfall {
if($script:wispfallStoredBlocks.Count-or$script:wispfallStoredGadgets.Count-or$script:wispfallStoredWires.Count){return}
Set-WireEditingVisible $false;Set-RopeEditingVisible $false
$script:wispfallStoredBlocks=@($script:playroomBlocks)
$script:wispfallStoredGadgets=@($script:playroomGadgets)
$script:wispfallStoredWires=@($script:playroomWires)
foreach($block in $script:wispfallStoredBlocks){$block.Window.Hide()}
foreach($gadget in $script:wispfallStoredGadgets){if($null-ne$gadget.Window){$gadget.Window.Hide()}}
foreach($wire in $script:wispfallStoredWires){if($null-ne$wire.Window){$wire.Window.Hide()}}
$script:playroomBlocks.Clear();$script:playroomGadgets.Clear();$script:playroomWires.Clear()
}
function Restore-SandboxObjectsAfterWispfall {
foreach($block in @($script:wispfallStoredBlocks)){
$script:playroomBlocks.Add($block)|Out-Null
if($block.TemporaryState-ne'Gone'){$block.Window.Show()}
}
foreach($gadget in @($script:wispfallStoredGadgets)){$script:playroomGadgets.Add($gadget)|Out-Null;if($null-ne$gadget.Window){$gadget.Window.Show()}}
foreach($wire in @($script:wispfallStoredWires)){$script:playroomWires.Add($wire)|Out-Null}
$script:wispfallStoredBlocks=@();$script:wispfallStoredGadgets=@();$script:wispfallStoredWires=@()
}
function Remove-WispfallSetup {
foreach($block in @($script:playroomBlocks|Where-Object{$_.WispfallGenerated})){Remove-PlayroomBlock $block}
foreach($gadget in @($script:playroomGadgets|Where-Object{$null-ne$_.PSObject.Properties['WispfallGenerated']-and$_.WispfallGenerated})){Remove-PlayroomPlacement $gadget}
Close-WispfallLava
$script:wispfallActive=$false
Restore-SandboxObjectsAfterWispfall
if($null-ne$script:wispfallPreviousScalePercent){
$restoreScale=[int]$script:wispfallPreviousScalePercent;$script:wispfallPreviousScalePercent=$null
if($script:petScalePercent-ne$restoreScale){Set-PetScale $restoreScale $false}
}
}
function Return-ToSandbox {
if($script:footballActive){
$script:footballActive=$false
Clear-PlayroomBalls
Remove-FootballSetup
}
if($script:wispfallActive-or$null-ne$script:wispfallLavaWindow){Remove-WispfallSetup}
# Return-ToSandbox is also used after ordinary Survival/Dodgeball sessions,
# where Stop-Playroom has disabled the editor state entirely. Re-enter the
# playroom before restoring its controls and visuals.
if(-not$script:playroomActive){Start-Playroom}
$script:gameType='Survival';$script:wispfallGameOver=$false;$script:footballMatchOver=$false
$script:active=$true;$script:hearts=[Math]::Max(1,$script:hearts)
Register-ControlKeys $true;Set-ClickThrough $false
Set-WireEditingVisible $true;Set-RopeEditingVisible $true
foreach($block in @($script:playroomBlocks)){Set-BlockGameplayVisual $block $false}
foreach($placement in @($script:playroomGadgets)){
if($placement.Kind-in@('Rotator','Rope')){$placement.Window.Show()}
else{Set-OverlayClickThroughState $placement.Window $false}
}
if($null-eq$script:sandboxHotbarWindow){New-SandboxToolbox}
Set-SandboxToolboxVisible $true
Update-PlatformHighlights;Update-Hud;Update-Menu
}
function Add-WispfallPlatform([double]$y) {
$previousY=$script:wispfallHighestY;$previousX=$script:wispfallLastX
$stage=[Math]::Floor($script:wispfallTicks/1800)
$types=$(if($stage-le0){@('Normal','Grass','Bouncy')}elseif($stage-eq1){@('Normal','Wooden','Bouncy','Cloud','Ice')}elseif($stage-eq2){@('Wooden','Cloud','Crumbling','Ice','Conveyor','Bouncy')}else{@('Cloud','Crumbling','Wooden','Conveyor','Bouncy','Fire')})
$type=$types[$script:random.Next(0,$types.Count)]
Add-PlayroomBlock $type
$block=@($script:playroomBlocks|Select-Object -Last 1)[0]
$screenLeft=[Windows.SystemParameters]::VirtualScreenLeft;$screenWidth=[Windows.SystemParameters]::VirtualScreenWidth
# Wispfall needs an open climbing route: platforms should leave enough vertical
# clearance for the pet and should lead the player across the whole desktop.
$sizeRoll=$script:random.NextDouble()
$block.Width=$(if($sizeRoll-lt.18){$script:random.Next(110,151)}elseif($sizeRoll-gt.82){$script:random.Next(270,351)}elseif($stage-le0){$script:random.Next(190,271)}elseif($stage-eq1){$script:random.Next(165,246)}else{$script:random.Next(140,216)})
$laneCount=[Math]::Max(6,[Math]::Min(10,[Math]::Floor($screenWidth/175)))
$laneWidth=($screenWidth-80)/$laneCount
if($script:wispfallLastLane-le0){$script:wispfallLaneDirection=1}
elseif($script:wispfallLastLane-ge($laneCount-1)){$script:wispfallLaneDirection=-1}
$laneStep=$(if($script:random.NextDouble()-lt.28){2}else{1})
$directedLane=$script:wispfallLastLane+($script:wispfallLaneDirection*$laneStep)
if($directedLane-lt0-or$directedLane-ge$laneCount){
$script:wispfallLaneDirection*=-1
$directedLane=$script:wispfallLastLane+($script:wispfallLaneDirection*$laneStep)
}
$directedLane=[Math]::Max(0,[Math]::Min($laneCount-1,$directedLane))
$lane=$(if($script:random.NextDouble()-lt.88){$directedLane}else{[Math]::Max(0,[Math]::Min($laneCount-1,$script:wispfallLastLane+$script:random.Next(-1,2)))})
$laneCentre=$screenLeft+40+(($lane+.5)*$laneWidth)
$jitter=[Math]::Min(34,$laneWidth*.2)
$candidate=$laneCentre-($block.Width/2)+(($script:random.NextDouble()*2-1)*$jitter)
$minX=$screenLeft+25;$maxX=$screenLeft+$screenWidth-$block.Width-25
$block.X=[Math]::Max($minX,[Math]::Min($maxX,$candidate));$block.Y=$y
$block.Height=$(if($type-eq'Wooden'){18}else{30});$block.WispfallGenerated=$true
# This resizes both the logical collision body and its painted WPF window.
Set-PlacementAngle $block 0;Set-BlockGameplayVisual $block $true
$ladderAdded=$false
if($previousY-gt$y-and($env:WINDOWISP_TEST_WISPFALL_VARIETY-eq'1'-or$script:random.NextDouble()-lt.12)){
$gap=$previousY-$y
Add-PlayroomBlock 'Ladder';$ladder=@($script:playroomBlocks|Select-Object -Last 1)[0]
$ladder.Width=56;$ladder.Height=[Math]::Max(105,$gap+18)
$ladder.X=[Math]::Max($screenLeft+10,[Math]::Min($screenLeft+$screenWidth-$ladder.Width-10,$block.X+($block.Width-$ladder.Width)/2))
$ladder.Y=$block.Y+6;$ladder.WispfallGenerated=$true;Set-PlacementAngle $ladder 0;Set-BlockGameplayVisual $ladder $true
$ladderAdded=$true
}
if(-not$ladderAdded-and$block.Width-ge170-and$type-notin@('Cloud','Crumbling','Fire')-and($env:WINDOWISP_TEST_WISPFALL_VARIETY-eq'1'-or$script:random.NextDouble()-lt.13)){
Add-PlayroomGadget 'Spring';$spring=@($script:playroomGadgets|Where-Object{$_.Kind-eq'Spring'}|Select-Object -Last 1)[0]
if($null-ne$spring){
$spring|Add-Member -NotePropertyName WispfallGenerated -NotePropertyValue $true -Force
$spring|Add-Member -NotePropertyName WispfallSupport -NotePropertyValue $block -Force
$spring.X=$block.X+($block.Width-$spring.Width)/2;$spring.Y=$block.Y-$spring.Height+8
Set-PlacementWindowPosition $spring;Set-OverlayClickThroughState $spring.Window $true
}
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_WISPFALL_VARIETY-eq'1'){
[Console]::Out.WriteLine("diagnostic:wispfall-platform type=$type logical=$([Math]::Round($block.Width))x$([Math]::Round($block.Height)) window=$([Math]::Round($block.Window.Width))x$([Math]::Round($block.Window.Height)) ladders=$(@($script:playroomBlocks|Where-Object{$_.WispfallGenerated-and$_.Type-eq'Ladder'}).Count) springs=$(@($script:playroomGadgets|Where-Object{$null-ne$_.PSObject.Properties['WispfallGenerated']-and$_.WispfallGenerated}).Count)")
}
$script:wispfallLastX=$block.X+($block.Width/2);$script:wispfallLastLane=$lane;$script:wispfallHighestY=$y
}
function Start-Wispfall {
if(-not$script:playroomActive){Start-Playroom}
if($script:footballActive){$script:footballActive=$false;Remove-FootballSetup}
Remove-WispfallSetup
$script:wispfallPreviousScalePercent=$script:petScalePercent
if($script:petScalePercent-ne70){Set-PetScale 70 $false}
Clear-PlayroomBalls
Hide-SandboxObjectsForWispfall
$script:wispfallActive=$true;$script:gameType='Wispfall';$script:wispfallTicks=0;$script:wispfallScore=0;$script:wispfallGameOver=$false
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop;$width=[Windows.SystemParameters]::VirtualScreenWidth;$height=[Windows.SystemParameters]::VirtualScreenHeight
$laneCount=[Math]::Max(6,[Math]::Min(10,[Math]::Floor($width/175)))
$script:wispfallLavaTop=$top+$height-55;$script:wispfallLastX=$left+($width/2);$script:wispfallLastLane=[Math]::Floor($laneCount/2);$script:wispfallLaneDirection=$(if($script:random.Next(0,2)-eq0){-1}else{1});$script:wispfallHighestY=$top+$height-130
$lava=New-Object Windows.Window;$lava.Title='Windowisp Wispfall Lava';$lava.Width=$width;$lava.Height=70;$lava.Left=$left;$lava.Top=$script:wispfallLavaTop
$lava.WindowStyle='None';$lava.AllowsTransparency=$true;$lava.Background=[Windows.Media.Brushes]::Transparent
$lava.Topmost=$true;$lava.ShowInTaskbar=$false;$lava.ShowActivated=$false;$lava.ResizeMode='NoResize';$lava.Show();Set-OverlayClickThrough $lava;$script:wispfallLavaWindow=$lava
$lavaGrid=New-Object Windows.Controls.Grid;$lavaGrid.ClipToBounds=$true;$lavaGrid.IsHitTestVisible=$false
$lavaDepth=New-Object Windows.Controls.Border
$lavaSource=Get-ToyboxImageSource 'WispfallLava'
if($null-ne$lavaSource){
$lavaBrush=New-Object Windows.Media.ImageBrush $lavaSource;$lavaBrush.TileMode='FlipXY';$lavaBrush.ViewportUnits='Absolute'
$lavaBrush.Viewport=[Windows.Rect]::new(0,0,330,330);$lavaBrush.Stretch='Fill';$lavaBrush.AlignmentX='Left';$lavaBrush.AlignmentY='Top'
$lavaDepth.Background=$lavaBrush
}else{$lavaDepth.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(255,171,18),[Windows.Media.Color]::FromRgb(116,5,30),90)}
$lavaGrid.Children.Add($lavaDepth)|Out-Null
$surface=New-Object Windows.Shapes.Path;$surface.Height=24;$surface.VerticalAlignment='Top';$surface.Stretch='Fill'
$surface.Data=[Windows.Media.Geometry]::Parse('M0,14 C35,0 70,25 108,10 C147,-4 184,23 222,8 C260,-5 300,23 340,9 C382,-4 420,24 464,8 C510,-5 552,21 600,10 L600,28 L0,28 Z')
$surface.Fill=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(255,250,92),[Windows.Media.Color]::FromRgb(255,112,12),90)
$surface.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,239,102));$surface.StrokeThickness=3
$lavaGrid.Children.Add($surface)|Out-Null
$bubbleCanvas=New-Object Windows.Controls.Canvas;$bubbleCanvas.ClipToBounds=$true
foreach($bubbleSpec in @(@(.06,20,12),@(.15,43,7),@(.27,28,16),@(.39,51,9),@(.52,31,13),@(.66,47,8),@(.78,23,15),@(.9,40,10),@(.96,16,6))){
$bubble=New-Object Windows.Shapes.Ellipse;$bubble.Width=$bubbleSpec[2];$bubble.Height=$bubbleSpec[2]
$bubble.Fill=New-Object Windows.Media.RadialGradientBrush ([Windows.Media.Color]::FromRgb(255,247,118),[Windows.Media.Color]::FromRgb(255,91,9))
$bubble.Stroke=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,202,35));$bubble.StrokeThickness=1.5
[Windows.Controls.Canvas]::SetLeft($bubble,$width*$bubbleSpec[0]);[Windows.Controls.Canvas]::SetTop($bubble,$bubbleSpec[1]);$bubbleCanvas.Children.Add($bubble)|Out-Null
}
$lavaGrid.Children.Add($bubbleCanvas)|Out-Null;$lava.Content=$lavaGrid
foreach($offset in @(0,150,300,450,600,750)){Add-WispfallPlatform ($top+$height-125-$offset)}
$start=@($script:playroomBlocks|Where-Object{$_.WispfallGenerated-and$_.Type-ne'Ladder'}|Sort-Object Y -Descending|Select-Object -First 1)[0]
$script:x=$start.X+20;$script:y=$start.Y-$PetHeight;$script:vx=0;$script:vy=0;$script:grounded=$true
if($script:twoPlayerActive){$script:p2X=$start.X+$start.Width-$PetWidth-20;$script:p2Y=$script:y;$script:p2VX=0;$script:p2VY=0}
Set-SandboxToolboxVisible $false;Set-WireEditingVisible $false;Set-RopeEditingVisible $false
foreach($block in @($script:playroomBlocks|Where-Object{$_.WispfallGenerated})){Set-BlockGameplayVisual $block $true}
$script:active=$true;Register-ControlKeys $true;Set-ClickThrough $false
if($null-ne$script:gameModePanel){$script:gameModePanel.Visibility='Collapsed'};if($null-ne$script:menuWindow){$script:menuWindow.Hide()}
Update-Hud;Update-Menu
}
function Update-Wispfall {
if(-not$script:wispfallActive-or$script:wispfallGameOver){return}
$script:wispfallTicks++;$script:wispfallScore=[Math]::Max($script:wispfallScore,[Math]::Floor($script:wispfallTicks/6))
$top=[Windows.SystemParameters]::VirtualScreenTop;$bottom=$top+[Windows.SystemParameters]::VirtualScreenHeight
$descent=[Math]::Min(1.45,.22+($script:wispfallTicks/9000.0));$rise=[Math]::Min(.34,.045+($script:wispfallTicks/30000.0))
$lavaLimit=$bottom-([Windows.SystemParameters]::VirtualScreenHeight*.15)
$script:wispfallLavaTop=[Math]::Max($lavaLimit,$script:wispfallLavaTop-$rise)
$script:wispfallLavaWindow.Top=$script:wispfallLavaTop;$script:wispfallLavaWindow.Height=$bottom-$script:wispfallLavaTop+8
foreach($block in @($script:playroomBlocks|Where-Object{$_.WispfallGenerated})){
$block.Y+=$descent;Set-PlacementWindowPosition $block
if($block.Y-gt$script:wispfallLavaTop+25){Remove-PlayroomBlock $block}
}
foreach($spring in @($script:playroomGadgets|Where-Object{$null-ne$_.PSObject.Properties['WispfallGenerated']-and$_.WispfallGenerated})){
$support=$spring.WispfallSupport
if($null-eq$support-or-not$script:playroomBlocks.Contains($support)-or$support.TemporaryState-eq'Gone'){
Remove-PlayroomPlacement $spring;continue
}
$spring.X=$support.X+($support.Width-$spring.Width)/2;$spring.Y=$support.Y-$spring.Height+8;Set-PlacementWindowPosition $spring
}
if($script:wispfallHighestY-gt$top+85){Add-WispfallPlatform ($script:wispfallHighestY-$script:random.Next(135,171))}
# Keep the generated stream filled above the screen as old platforms descend.
$script:wispfallHighestY+=$descent
Update-TemporaryBlocks
$p1Out=$script:y+$PetHeight-12-ge$script:wispfallLavaTop
$p2Out=$script:twoPlayerActive-and$script:p2Y+$PetHeight-12-ge$script:wispfallLavaTop
if($p1Out-and(-not$script:twoPlayerActive-or$p2Out)){
$script:wispfallGameOver=$true;$script:active=$false;Register-ControlKeys $false
Close-WispfallLava
Update-Hud;Update-Menu
}
}
function Start-GameType([string]$type) {
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:start-game-type-begin type=$type playroom=$($script:playroomActive)")}
if($type-eq'Football'){Start-Football;return}
if($type-eq'Wispfall'){Start-Wispfall;return}
if ($type -notin @('Survival','Dodgeball')) { $type = 'Survival' }
if($script:footballActive){$script:footballActive=$false;Remove-FootballSetup}
if($script:wispfallActive){Remove-WispfallSetup}
if ($script:playroomActive) { Stop-Playroom }
$script:gameType = $type
Reset-Game
Reset-PetPosition
Set-GameMode $true
$script:lastRenderKey = ''
if ($null -ne $script:gameModePanel) { $script:gameModePanel.Visibility = [Windows.Visibility]::Collapsed }
Update-Hud; Update-Menu
Hide-ControlOverlay
if ($null -ne $script:controlsWindow) { $script:controlsWindow.Close(); $script:controlsWindow = $null }
[Windows.Input.Keyboard]::ClearFocus()
if ($null -ne $script:menuWindow -and $script:menuWindow.IsVisible) { $script:menuWindow.Hide() }
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:start-game-type-end type=$type active=$($script:active)")}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output "diagnostic:game-type $type" }
}
function Restart-CurrentGame { Start-GameType $script:gameType }
function Restart-SurvivalGame {
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output "diagnostic:survival-restart playroom=$($script:playroomActive) active=$($script:active)" }
Start-GameType 'Survival'
}
function Show-Controls {
if ($null -ne $script:controlsWindow -and $script:controlsWindow.IsVisible) {
$script:controlsWindow.Activate() | Out-Null
return
}
$script:controlsWindow = New-Object Windows.Window
$script:controlsWindow.Title = 'Windowisp Controls'
$script:controlsWindow.Width = 386; $script:controlsWindow.Height = 565
$script:controlsWindow.WindowStyle = [Windows.WindowStyle]::None
$script:controlsWindow.AllowsTransparency = $true
$script:controlsWindow.Background = [Windows.Media.Brushes]::Transparent
$script:controlsWindow.ShowInTaskbar = $false; $script:controlsWindow.Topmost = $script:alwaysOnTop
$script:controlsWindow.ResizeMode = [Windows.ResizeMode]::NoResize
$layout = New-Object Windows.Controls.Grid
$tail = New-Object Windows.Shapes.Polygon
$tail.Points = New-Object Windows.Media.PointCollection
$tail.Points.Add([Windows.Point]::new(0, 80))
$tail.Points.Add([Windows.Point]::new(18, 68))
$tail.Points.Add([Windows.Point]::new(18, 92))
$tail.Fill = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(31, 28, 45))
$tail.HorizontalAlignment = [Windows.HorizontalAlignment]::Left
$tail.VerticalAlignment = [Windows.VerticalAlignment]::Top
$layout.Children.Add($tail) | Out-Null
$bubble = New-Object Windows.Controls.Border
$bubble.Margin = [Windows.Thickness]::new(16, 0, 0, 0)
$bubble.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(31, 28, 45))
$bubble.BorderBrush = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(91, 78, 121))
$bubble.BorderThickness = [Windows.Thickness]::new(1)
$bubble.CornerRadius = [Windows.CornerRadius]::new(18)
$bubble.Effect = New-Object Windows.Media.Effects.DropShadowEffect
$bubble.Effect.Color = [Windows.Media.Colors]::Black
$bubble.Effect.BlurRadius = 24; $bubble.Effect.ShadowDepth = 5; $bubble.Effect.Opacity = 0.42
$content = New-Object Windows.Controls.StackPanel
$content.Margin = [Windows.Thickness]::new(22, 16, 22, 18)
$back = New-Object Windows.Controls.Button
$back.Content = ([string][char]0x2190) + ' Back'
$back.HorizontalAlignment = [Windows.HorizontalAlignment]::Left
$back.Background = [Windows.Media.Brushes]::Transparent
$back.Foreground = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255, 178, 112))
$back.BorderThickness = [Windows.Thickness]::new(0)
$back.FontSize = 14; $back.FontWeight = [Windows.FontWeights]::SemiBold
$back.Cursor = [Windows.Input.Cursors]::Hand
$back.Add_Click({ $script:controlsWindow.Close(); $script:controlsWindow = $null })
$content.Children.Add($back) | Out-Null
$heading = New-Object Windows.Controls.TextBlock
$heading.Text = 'CONTROLS'; $heading.Foreground = [Windows.Media.Brushes]::White
$heading.FontSize = 20; $heading.FontWeight = [Windows.FontWeights]::Bold
$heading.Margin = [Windows.Thickness]::new(2, 10, 0, 12)
$content.Children.Add($heading) | Out-Null
$controls = @(
@('A / D or Left / Right', 'Move - double-tap in air to dash'),
@('W / Space / Up', 'Jump - double jump or leap from blocks / screen edges'),
@('S or Down', 'Drop through wooden platforms'),
@('J', 'Hold to charge - release to fire'),
@('E', 'Pick up / throw a nearby ball'),
@('F7', 'Freeze frame + click-through'),
@('F8', 'Pause / resume Game Mode'),
@('F9', 'Restart the round'),
@('F10', 'Close Windowisp'),
@('Ctrl + Alt + P', 'Open or close the Control Centre'),
@('Esc', 'Exit Game Mode'),
@('P2 Arrows / Down', 'Move and drop - Numpad preset'),
@('P2 Numpad 0 / + / 1', 'Jump / attack / interact'),
@('P2 I / J / K / L / O / U', 'Laptop movement, attack + interact')
)
foreach ($entry in $controls) {
$row = New-Object Windows.Controls.Grid
$row.Margin = [Windows.Thickness]::new(2, 3, 2, 3)
$row.ColumnDefinitions.Add((New-Object Windows.Controls.ColumnDefinition -Property @{ Width = [Windows.GridLength]::new(132) }))
$row.ColumnDefinitions.Add((New-Object Windows.Controls.ColumnDefinition -Property @{ Width = [Windows.GridLength]::new(1, [Windows.GridUnitType]::Star) }))
$key = New-Object Windows.Controls.TextBlock
$key.Text = $entry[0]; $key.Foreground = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255, 190, 132))
$key.FontWeight = [Windows.FontWeights]::SemiBold; $key.FontSize = 12
$action = New-Object Windows.Controls.TextBlock
$action.Text = $entry[1]; $action.Foreground = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(224, 219, 235))
$action.FontSize = 12; $action.TextWrapping = [Windows.TextWrapping]::Wrap
[Windows.Controls.Grid]::SetColumn($action, 1)
$row.Children.Add($key) | Out-Null; $row.Children.Add($action) | Out-Null
$content.Children.Add($row) | Out-Null
}
$tip = New-Object Windows.Controls.TextBlock
$tip.Text = 'Tip: drag the dotted grip beside the paw to move the hub.'
$tip.Foreground = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(167, 158, 188))
$tip.FontSize = 11; $tip.Margin = [Windows.Thickness]::new(2, 12, 2, 0)
$content.Children.Add($tip) | Out-Null
$bubble.Child = $content; $layout.Children.Add($bubble) | Out-Null
$script:controlsWindow.Content = $layout
$screenLeft = [Windows.SystemParameters]::VirtualScreenLeft
$screenRight = $screenLeft + [Windows.SystemParameters]::VirtualScreenWidth
$screenTop = [Windows.SystemParameters]::VirtualScreenTop
$screenBottom = $screenTop + [Windows.SystemParameters]::VirtualScreenHeight
$menuLeft = [Math]::Min($script:menuWindow.Left, $screenRight - $script:menuWindow.Width - $script:controlsWindow.Width - 20)
$script:menuWindow.Left = [Math]::Max($screenLeft + 8, $menuLeft)
$script:controlsWindow.Left = $script:menuWindow.Left + $script:menuWindow.Width + 6
$script:controlsWindow.Top = [Math]::Max($screenTop + 8, [Math]::Min($screenBottom - $script:controlsWindow.Height - 8, $script:menuWindow.Top + 18))
$script:controlsWindow.Opacity = 0
$script:controlsWindow.Show()
$fade = New-Object Windows.Media.Animation.DoubleAnimation
$fade.From = 0; $fade.To = 1; $fade.Duration = [Windows.Duration]::new([TimeSpan]::FromMilliseconds(140))
$script:controlsWindow.BeginAnimation([Windows.Window]::OpacityProperty, $fade)
}
function Get-ControlKeyName([int]$virtualKey) {
switch ($virtualKey) {
0x20 { return 'Space' }
0x25 { return 'Left' }
0x26 { return 'Up' }
0x27 { return 'Right' }
0x28 { return 'Down' }
0x60 { return 'Numpad 0' }
0x61 { return 'Numpad 1' }
0x6B { return 'Numpad +' }
default {
$name = [Windows.Input.KeyInterop]::KeyFromVirtualKey($virtualKey).ToString()
if ($name -match '^D([0-9])$') { return $Matches[1] }
return $name
}
}
}
function Save-ControlBindings {
try {
$configDirectory = Split-Path -Parent $script:controlsConfigPath
if (-not (Test-Path -LiteralPath $configDirectory -PathType Container)) {
New-Item -ItemType Directory -Path $configDirectory -Force | Out-Null
}
$script:controlBindings | ConvertTo-Json | Set-Content -LiteralPath $script:controlsConfigPath -Encoding UTF8
} catch {}
}
function Update-ControlBindingButtons {
foreach ($actionName in @($script:controlsBindingButtons.Keys)) {
$button = $script:controlsBindingButtons[$actionName]
$button.Content = Get-ControlKeyName ([int]$script:controlBindings[$actionName])
}
}
function Hide-ControlOverlay {
$script:capturingBinding = $null
Update-ControlBindingButtons
if ($null -ne $script:controlsOverlay) { $script:controlsOverlay.Visibility = [Windows.Visibility]::Collapsed }
}
function Show-ControlOverlay {
if ($null -eq $script:controlsOverlay) { return }
Update-ControlBindingButtons
$script:controlsOverlay.Visibility = [Windows.Visibility]::Visible
[Windows.Controls.Panel]::SetZIndex($script:controlsOverlay, 20)
$script:menuWindow.Activate() | Out-Null
$script:controlsOverlay.Focus() | Out-Null
}
function New-ControlsOverlay {
$overlay = New-Object Windows.Controls.Border
$overlay.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(255,26,23,38))
$overlay.BorderBrush = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(91,78,121))
$overlay.BorderThickness = [Windows.Thickness]::new(1)
$overlay.Visibility = [Windows.Visibility]::Collapsed
$overlay.Focusable = $true
$layout = New-Object Windows.Controls.DockPanel
$header = New-Object Windows.Controls.Grid
$header.Height = 58; $header.Margin = [Windows.Thickness]::new(10,4,10,0)
$back = New-Object Windows.Controls.Button
$back.Content = ([string][char]0x2190); $back.Width=38; $back.Height=34
$back.HorizontalAlignment='Left'; $back.Background=[Windows.Media.Brushes]::Transparent
$back.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,178,112))
$back.BorderThickness=[Windows.Thickness]::new(0);$back.FontSize=22;$back.Cursor=[Windows.Input.Cursors]::Hand
$back.Add_Click({ Hide-ControlOverlay })
$title = New-Object Windows.Controls.TextBlock
$title.Text='CONTROLS';$title.Foreground=[Windows.Media.Brushes]::White;$title.FontSize=17
$title.FontWeight=[Windows.FontWeights]::Bold;$title.HorizontalAlignment='Center';$title.VerticalAlignment='Center'
$header.Children.Add($back)|Out-Null;$header.Children.Add($title)|Out-Null
[Windows.Controls.DockPanel]::SetDock($header,[Windows.Controls.Dock]::Top)
$layout.Children.Add($header)|Out-Null
$scroll = New-Object Windows.Controls.ScrollViewer
$scroll.VerticalScrollBarVisibility=[Windows.Controls.ScrollBarVisibility]::Auto
$scroll.HorizontalScrollBarVisibility=[Windows.Controls.ScrollBarVisibility]::Disabled
$content = New-Object Windows.Controls.StackPanel
$content.Margin=[Windows.Thickness]::new(12,0,12,14)
$hint = New-Object Windows.Controls.TextBlock
$hint.Text='Click a key, then press its replacement.';$hint.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(177,168,196))
$hint.FontSize=11;$hint.Margin=[Windows.Thickness]::new(4,0,4,8)
$content.Children.Add($hint)|Out-Null
$definitions = @(
@('PLAYER 1',$null),
@('Move Left','P1Left'),@('Move Right','P1Right'),@('Jump','P1Jump'),@('Drop','P1Drop'),@('Attack','P1Attack'),@('Interact / Throw','P1Interact'),
@('PLAYER 2',$null),
@('Move Left','P2Left'),@('Move Right','P2Right'),@('Jump','P2Jump'),@('Drop','P2Drop'),@('Attack','P2Attack'),@('Interact / Throw','P2Interact')
)
foreach ($definition in $definitions) {
if ($null -eq $definition[1]) {
$section = New-Object Windows.Controls.TextBlock
$section.Text=$definition[0];$section.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,178,112))
$section.FontSize=11;$section.FontWeight=[Windows.FontWeights]::Bold;$section.Margin=[Windows.Thickness]::new(4,8,4,4)
$content.Children.Add($section)|Out-Null
continue
}
$row = New-Object Windows.Controls.Grid
$row.Height=38;$row.Margin=[Windows.Thickness]::new(2,2,2,2)
$row.ColumnDefinitions.Add((New-Object Windows.Controls.ColumnDefinition -Property @{Width=[Windows.GridLength]::new(1,[Windows.GridUnitType]::Star)}))
$row.ColumnDefinitions.Add((New-Object Windows.Controls.ColumnDefinition -Property @{Width=[Windows.GridLength]::new(102)}))
$action = New-Object Windows.Controls.TextBlock
$action.Text=$definition[0];$action.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(225,220,236))
$action.FontSize=12;$action.VerticalAlignment='Center';$action.Margin=[Windows.Thickness]::new(8,0,4,0)
$keyButton = New-Object Windows.Controls.Button
$keyButton.Tag=$definition[1];$keyButton.Margin=[Windows.Thickness]::new(3)
$keyButton.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(55,48,73))
$keyButton.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,190,132))
$keyButton.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(106,89,136))
$keyButton.BorderThickness=[Windows.Thickness]::new(1);$keyButton.Cursor=[Windows.Input.Cursors]::Hand
$keyButton.Add_Click({
param($sender,$eventArgs)
if ($null -ne $script:capturingBinding -and $script:controlsBindingButtons.ContainsKey($script:capturingBinding)) {
Update-ControlBindingButtons
}
$script:capturingBinding=[string]$sender.Tag
$sender.Content='Press key...'
$script:menuWindow.Activate()|Out-Null
$script:controlsOverlay.Focus()|Out-Null
})
[Windows.Controls.Grid]::SetColumn($keyButton,1)
$row.Children.Add($action)|Out-Null;$row.Children.Add($keyButton)|Out-Null
$script:controlsBindingButtons[$definition[1]]=$keyButton
$content.Children.Add($row)|Out-Null
}
$reset = New-MenuButton 'Reset Controls' {
$script:controlBindings=$script:controlDefaults.Clone()
$script:p2Preset='Numpad'
Save-ControlBindings; Update-ControlBindingButtons; Update-Menu
}
$content.Children.Add($reset)|Out-Null
$scroll.Content=$content;$layout.Children.Add($scroll)|Out-Null
$overlay.Child=$layout
$overlay.Add_PreviewKeyDown({
param($sender,$eventArgs)
if ($null -eq $script:capturingBinding) { return }
$pressedKey = $(if ($eventArgs.Key -eq [Windows.Input.Key]::System) { $eventArgs.SystemKey } else { $eventArgs.Key })
if ($pressedKey -eq [Windows.Input.Key]::Escape) {
$script:capturingBinding=$null;Update-ControlBindingButtons;$eventArgs.Handled=$true;return
}
$virtualKey=[Windows.Input.KeyInterop]::VirtualKeyFromKey($pressedKey)
if ($virtualKey -gt 0) {
$script:controlBindings[$script:capturingBinding]=[int]$virtualKey
$script:capturingBinding=$null;Save-ControlBindings;Update-ControlBindingButtons
$eventArgs.Handled=$true
}
})
$script:controlsOverlay=$overlay
Update-ControlBindingButtons
return $overlay
}
function Set-AlwaysOnTop([bool]$enabled) {
$script:alwaysOnTop = $enabled
$window.Topmost = $enabled
if ($null -ne $script:menuButtonWindow) { $script:menuButtonWindow.Topmost = $enabled }
if ($null -ne $script:menuWindow) { $script:menuWindow.Topmost = $enabled }
if ($null -ne $script:controlsWindow) { $script:controlsWindow.Topmost = $enabled }
if ($null -ne $script:player2Window) { $script:player2Window.Topmost = $enabled }
if ($null -ne $script:sandboxHotbarWindow) { $script:sandboxHotbarWindow.Topmost = $enabled }
if ($null -ne $script:gridOverlayWindow) { $script:gridOverlayWindow.Topmost = $enabled }
if ($null -ne $script:sandboxTrayWindow) { $script:sandboxTrayWindow.Topmost = $enabled }
if ($null -ne $script:petSelectorWindow) { $script:petSelectorWindow.Topmost = $enabled }
if ($null -ne $script:ropeOverlayWindow) { $script:ropeOverlayWindow.Topmost = $enabled }
foreach($gadget in @($script:playroomGadgets)){if($null-ne$gadget.Window){$gadget.Window.Topmost=$enabled}}
}
function Set-Frozen([bool]$enabled) {
$script:frozen = $enabled
Register-ControlKeys ($script:active -and -not $script:frozen)
Set-ClickThrough ($script:frozen -or -not $script:active)
Update-Menu
}
function Set-ActivePetAtlas([string]$path,[string]$id,[string]$displayName,[string]$player='P1') {
if(-not(Test-Path -LiteralPath $path -PathType Leaf)){return}
$newAtlas=New-Object Windows.Media.Imaging.BitmapImage
$newAtlas.BeginInit();$newAtlas.CacheOption=[Windows.Media.Imaging.BitmapCacheOption]::OnLoad
$newAtlas.UriSource=[Uri](Resolve-Path -LiteralPath $path).Path
$newAtlas.EndInit();$newAtlas.Freeze()
if($newAtlas.PixelWidth-ne1536-or$newAtlas.PixelHeight-ne2288){return}
if($player-eq'P2'){
$script:p2Atlas=$newAtlas;$script:p2PetId=$id;$script:p2PetName=$displayName
$script:p2SpriteFrameCache.Clear();$script:p2LastRenderKey=''
if($null-ne$script:p2StatusPortrait){$script:p2StatusPortrait.Source=Get-P2SpriteFrame 0 0}
if($script:twoPlayerActive-and$null-ne$script:player2Window){Draw-Player2 'idle' $(if($script:p2Facing-eq0){1}else{$script:p2Facing}) 0}
}else{
$script:atlas=$newAtlas;$script:activePetId=$id;$script:activePetName=$displayName
$script:spriteFrameCache.Clear();$script:lastRenderKey=''
if($null-ne$script:p1StatusPortrait){$script:p1StatusPortrait.Source=Get-PetSpriteFrame 0 0}
}
Update-PetSelectorButton
if($null-ne$script:petSelectorWindow-and$script:petSelectorWindow.IsVisible){Show-PetSelector}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:pet-switch player=$player id=$id name=$displayName")}
}
function Get-PersonalPetCatalog {
$userProfile=[Environment]::GetFolderPath('UserProfile')
if(-not[string]::IsNullOrWhiteSpace($env:USERPROFILE)-and(Test-Path -LiteralPath $env:USERPROFILE -PathType Container)){
$userProfile=$env:USERPROFILE
}
$petRoot=$(if(-not[string]::IsNullOrWhiteSpace($env:WINDOWISP_PET_ROOT)){$env:WINDOWISP_PET_ROOT}else{Join-Path $userProfile '.codex\pets'})
$cacheRoot=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Windowisp\pet-cache'
$personalCatalog=@()
if(Test-Path -LiteralPath $petRoot -PathType Container){
foreach($manifestPath in Get-ChildItem -LiteralPath $petRoot -Filter pet.json -File -Recurse -ErrorAction SilentlyContinue){
try{
$manifest=Get-Content -LiteralPath $manifestPath.FullName -Raw|ConvertFrom-Json
if([int]$manifest.spriteVersionNumber-ne2-or[string]::IsNullOrWhiteSpace([string]$manifest.id)){continue}
$safeId=([string]$manifest.id)-replace'[^a-zA-Z0-9._-]','_'
$personalCatalog+=[pscustomobject]@{
Id=[string]$manifest.id
DisplayName=$(if($manifest.displayName){[string]$manifest.displayName}else{[string]$manifest.id})
CachePath=(Join-Path $cacheRoot "$safeId.png")
SourcePath=(Join-Path $manifestPath.DirectoryName ([string]$manifest.spritesheetPath))
Updated=$manifestPath.LastWriteTimeUtc
IsStarter=$false;IsBuiltIn=$false
}
}catch{}
}
}
$builtInCatalog=@()
$builtInRoot=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Windowisp\codex-default-pets'
foreach($builtInSpec in @(@('codex','Codex'),@('dewey','Dewey'),@('fireball','Fireball'),@('hoots','Hoots'),@('rocky','Rocky'),@('seedy','Seedy'),@('stacky','Stacky'),@('bsod','BSOD'),@('null-signal','Null Signal'))){
$builtInId=[string]$builtInSpec[0];$builtInSource=Join-Path $builtInRoot "$builtInId.webp"
if((-not(Test-Path -LiteralPath $builtInSource -PathType Leaf))-or@($personalCatalog|Where-Object{$_.Id-eq$builtInId}).Count-gt0){continue}
$builtInCatalog+=[pscustomobject]@{Id=$builtInId;DisplayName=[string]$builtInSpec[1];CachePath=(Join-Path $cacheRoot "$builtInId.png");SourcePath=$builtInSource;Updated=(Get-Item -LiteralPath $builtInSource).LastWriteTimeUtc;IsStarter=$false;IsBuiltIn=$true}
}
$starterCatalog=@()
$starterManifestPath=Join-Path $PSScriptRoot 'assets\caiuto-cub\pet.json'
if(Test-Path -LiteralPath $starterManifestPath -PathType Leaf){
try{
$starterManifest=Get-Content -LiteralPath $starterManifestPath -Raw|ConvertFrom-Json
$starterSource=Join-Path (Split-Path -Parent $starterManifestPath) ([string]$starterManifest.spritesheetPath)
if([int]$starterManifest.spriteVersionNumber-eq2-and(Test-Path -LiteralPath $starterSource -PathType Leaf)-and-not($personalCatalog|Where-Object{$_.Id-eq[string]$starterManifest.id})){
$starterCatalog+=[pscustomobject]@{Id=[string]$starterManifest.id;DisplayName=$(if($starterManifest.displayName){[string]$starterManifest.displayName}else{[string]$starterManifest.id});CachePath=$starterSource;SourcePath=$starterSource;Updated=[DateTime]::MinValue;IsStarter=$true;IsBuiltIn=$false}
}
}catch{}
}
return @($personalCatalog|Sort-Object DisplayName)+@($builtInCatalog)+@($starterCatalog)
}
function Get-PetPreviewImage([string]$cachePath,[string]$sourcePath,[string]$id) {
if($script:petSelectorTarget-eq'P2'-and$id-eq$script:p2PetId-and$null-ne$script:p2Atlas){
return Get-P2SpriteFrame 0 0
}
if($id-eq$script:activePetId-and$null-ne$script:atlas){
return Get-PetSpriteFrame 0 0
}
foreach($previewPath in @($cachePath,$sourcePath)){
if(-not(Test-Path -LiteralPath $previewPath -PathType Leaf)){continue}
try{
$previewAtlas=New-Object Windows.Media.Imaging.BitmapImage
$previewAtlas.BeginInit();$previewAtlas.CacheOption=[Windows.Media.Imaging.BitmapCacheOption]::OnLoad
$previewAtlas.UriSource=[Uri](Resolve-Path -LiteralPath $previewPath).Path
$previewAtlas.EndInit();$previewAtlas.Freeze()
if($previewAtlas.PixelWidth-eq1536-and$previewAtlas.PixelHeight-eq2288){
return [Windows.Media.Imaging.CroppedBitmap]::new($previewAtlas,[Windows.Int32Rect]::new(0,0,192,208))
}
}catch{}
}
return $null
}
function Set-PetScale([int]$percent,[bool]$save=$true) {
if($percent-notin@(70,85,100,115)){return}
$oldWidth=$PetWidth;$oldHeight=$PetHeight
$p1Center=$script:x+($oldWidth/2);$p1Bottom=$script:y+$oldHeight
$p2Center=$script:p2X+($oldWidth/2);$p2Bottom=$script:p2Y+$oldHeight
$script:petScalePercent=$percent
$script:PetWidth=[int][Math]::Round($script:basePetWidth*($percent/100.0))
$script:PetHeight=[int][Math]::Round($script:basePetHeight*($percent/100.0))
$script:x=$p1Center-($script:PetWidth/2);$script:y=$p1Bottom-$script:PetHeight
$window.Width=$script:PetWidth;$window.Height=$script:PetHeight
$pet.Width=$script:PetWidth;$pet.Height=$script:PetHeight
if($script:twoPlayerActive-and$null-ne$script:player2Window){
$script:p2X=$p2Center-($script:PetWidth/2);$script:p2Y=$p2Bottom-$script:PetHeight
$script:player2Window.Width=$script:PetWidth;$script:player2Window.Height=$script:PetHeight
}
$script:lastRenderKey='';$script:p2LastRenderKey=''
if($save){
try{
$directory=Split-Path -Parent $script:petScalePath
if(-not(Test-Path -LiteralPath $directory -PathType Container)){New-Item -ItemType Directory -Path $directory -Force|Out-Null}
@{percent=$percent}|ConvertTo-Json|Set-Content -LiteralPath $script:petScalePath -Encoding UTF8
}catch{}
}
if($null-ne$script:petSelectorWindow-and$script:petSelectorWindow.IsVisible){Show-PetSelector}
}
function Update-PetSelectorButton {
if($null-eq$script:petSelectorButton){return}
$script:petSelectorButton.Content=([string][char]::ConvertFromUtf32(0x1F43A))
$script:petSelectorButton.ToolTip=$(if($script:twoPlayerActive){"Choose player pets - P1: $($script:activePetName) | P2: $($script:p2PetName)"}else{"Choose player pet - P1: $($script:activePetName)"})
}
function Select-PetForPlayer($entry,[string]$targetPlayer) {
if($null-eq$entry-or$targetPlayer-notin@('P1','P2')){return $false}
$currentId=$(if($targetPlayer-eq'P2'){$script:p2PetId}else{$script:activePetId})
if($entry.Id-eq$currentId){return $true}
if(Test-Path -LiteralPath $entry.CachePath -PathType Leaf){
Set-ActivePetAtlas $entry.CachePath $entry.Id $entry.DisplayName $targetPlayer|Out-Null
return $(if($targetPlayer-eq'P2'){$script:p2PetId-eq$entry.Id}else{$script:activePetId-eq$entry.Id})
}
$launcher=Join-Path $PSScriptRoot 'game-mode.ps1'
$arguments=@('-NoProfile','-ExecutionPolicy','Bypass','-File',"`"$launcher`"",'-Action','Switch','-PetId',"`"$($entry.Id)`"",'-Player',$targetPlayer)
Start-Process powershell.exe -ArgumentList $arguments -WindowStyle Hidden|Out-Null
return $true
}
function Show-PetSelector {
if($null-ne$script:menuWindow-and$script:menuWindow.IsVisible){$script:menuWindow.Hide()}
if($null-eq$script:petSelectorWindow){
$script:petSelectorWindow=New-Object Windows.Window
$script:petSelectorWindow.Title='Windowisp Pet Selector'
$script:petSelectorWindow.Width=420;$script:petSelectorWindow.Height=330
$script:petSelectorWindow.WindowStyle=[Windows.WindowStyle]::None
$script:petSelectorWindow.AllowsTransparency=$true
$script:petSelectorWindow.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(29,62,90),[Windows.Media.Color]::FromRgb(16,34,54),90)
$script:petSelectorWindow.Topmost=$true;$script:petSelectorWindow.ShowInTaskbar=$false
$script:petSelectorWindow.ResizeMode=[Windows.ResizeMode]::NoResize
$root=New-Object Windows.Controls.DockPanel
$top=New-Object Windows.Controls.StackPanel
[Windows.Controls.DockPanel]::SetDock($top,[Windows.Controls.Dock]::Top);$root.Children.Add($top)|Out-Null
$header=New-Object Windows.Controls.TextBlock
$cloudGlyph=[string][char]0x2601
$header.Text="$cloudGlyph CHOOSE YOUR WINDOWISP $cloudGlyph";$header.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(200,235,253))
$header.FontSize=14;$header.FontWeight=[Windows.FontWeights]::Bold;$header.Margin=[Windows.Thickness]::new(14,12,10,8)
$top.Children.Add($header)|Out-Null
$players=New-Object Windows.Controls.StackPanel;$players.Orientation=[Windows.Controls.Orientation]::Horizontal
$players.Margin=[Windows.Thickness]::new(12,0,8,6);$top.Children.Add($players)|Out-Null
$script:petSelectorP1Button=New-Object Windows.Controls.Button
$script:petSelectorP1Button.Content='Choose for P1';$script:petSelectorP1Button.Width=112;$script:petSelectorP1Button.Height=28
$script:petSelectorP1Button.Margin=[Windows.Thickness]::new(0,0,6,0);$script:petSelectorP1Button.Add_Click({$script:petSelectorTarget='P1';Show-PetSelector})
$players.Children.Add($script:petSelectorP1Button)|Out-Null
$script:petSelectorP2Button=New-Object Windows.Controls.Button
$script:petSelectorP2Button.Content='Choose for P2';$script:petSelectorP2Button.Width=112;$script:petSelectorP2Button.Height=28
$script:petSelectorP2Button.Add_Click({$script:petSelectorTarget='P2';Show-PetSelector})
$players.Children.Add($script:petSelectorP2Button)|Out-Null
$sizeLabel=New-Object Windows.Controls.TextBlock
$sizeLabel.Text='PET SIZE - applies to both players';$sizeLabel.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(166,203,226))
$sizeLabel.FontSize=10;$sizeLabel.FontWeight=[Windows.FontWeights]::SemiBold;$sizeLabel.Margin=[Windows.Thickness]::new(14,3,10,5)
$top.Children.Add($sizeLabel)|Out-Null
$sizeGrid=New-Object Windows.Controls.Primitives.UniformGrid;$sizeGrid.Rows=1;$sizeGrid.Columns=4
$sizeGrid.Margin=[Windows.Thickness]::new(12,0,12,8)
foreach($sizeOption in @(@('Small',70),@('Compact',85),@('Normal',100),@('Large',115))){
$sizeButton=New-Object Windows.Controls.Button
$sizeButton.Content="$($sizeOption[0]) $($sizeOption[1])%";$sizeButton.Height=28;$sizeButton.Margin=[Windows.Thickness]::new(2,0,2,0)
$sizeButton.Tag=[int]$sizeOption[1];$sizeButton.Foreground=[Windows.Media.Brushes]::White;$sizeButton.Cursor=[Windows.Input.Cursors]::Hand
$sizeButton.Add_Click({param($sender,$eventArgs)Set-PetScale ([int]$sender.Tag) $true})
$script:petSizeButtons+=,$sizeButton;$sizeGrid.Children.Add($sizeButton)|Out-Null
}
$top.Children.Add($sizeGrid)|Out-Null
$scroll=New-Object Windows.Controls.ScrollViewer
$scroll.VerticalScrollBarVisibility=[Windows.Controls.ScrollBarVisibility]::Auto
$script:petSelectorPanel=New-Object Windows.Controls.WrapPanel
$script:petSelectorPanel.Margin=[Windows.Thickness]::new(8)
$scroll.Content=$script:petSelectorPanel;$root.Children.Add($scroll)|Out-Null
$script:petSelectorWindow.Content=$root
}
if(-not$script:twoPlayerActive){$script:petSelectorTarget='P1'}
$script:petSelectorP2Button.Visibility=$(if($script:twoPlayerActive){[Windows.Visibility]::Visible}else{[Windows.Visibility]::Collapsed})
foreach($sizeButton in @($script:petSizeButtons)){
$selectedSize=[int]$sizeButton.Tag-eq$script:petScalePercent
$sizeButton.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb($(if($selectedSize){53}else{31}),$(if($selectedSize){135}else{65}),$(if($selectedSize){181}else{92})))
$sizeButton.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb($(if($selectedSize){151}else{83}),$(if($selectedSize){211}else{135}),$(if($selectedSize){242}else{169})))
}
foreach($pair in @(@($script:petSelectorP1Button,'P1'),@($script:petSelectorP2Button,'P2'))){
$selected=$script:petSelectorTarget-eq$pair[1]
$pair[0].Foreground=[Windows.Media.Brushes]::White
$pair[0].Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb($(if($selected){47}else{31}),$(if($selected){121}else{65}),$(if($selected){174}else{92})))
$pair[0].BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(116,187,230))
}
$selectedPetId=$(if($script:petSelectorTarget-eq'P2'){$script:p2PetId}else{$script:activePetId})
$script:petSelectorPanel.Children.Clear()
$selectedPetTile=$null
$currentPetGroup=''
foreach($entry in @(Get-PersonalPetCatalog)){
$petGroup=$(if($entry.IsStarter){'WINDOWISP STARTER'}elseif($entry.IsBuiltIn){'CODEX PETS'}else{'YOUR PETS'})
if($petGroup-ne$currentPetGroup){
$groupHeader=New-Object Windows.Controls.TextBlock
$groupHeader.Text=$petGroup;$groupHeader.Width=372;$groupHeader.Height=24
$groupHeader.Margin=[Windows.Thickness]::new(7,8,7,0)
$groupHeader.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(151,211,242))
$groupHeader.FontSize=10;$groupHeader.FontWeight=[Windows.FontWeights]::Bold
$script:petSelectorPanel.Children.Add($groupHeader)|Out-Null
$currentPetGroup=$petGroup
}
$tile=New-Object Windows.Controls.Button
$tile.Width=116;$tile.Height=128;$tile.Margin=[Windows.Thickness]::new(6)
$tile.Tag=$entry;$tile.Cursor=[Windows.Input.Cursors]::Hand
$tile.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb($(if($entry.Id-eq$selectedPetId){51}else{36}),$(if($entry.Id-eq$selectedPetId){103}else{68}),$(if($entry.Id-eq$selectedPetId){143}else{94})),[Windows.Media.Color]::FromRgb(20,42,63),90)
$tile.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb($(if($entry.Id-eq$selectedPetId){126}else{78}),$(if($entry.Id-eq$selectedPetId){204}else{132}),$(if($entry.Id-eq$selectedPetId){246}else{164})))
$tile.BorderThickness=[Windows.Thickness]::new($(if($entry.Id-eq$selectedPetId){2}else{1}))
if($entry.Id-eq$selectedPetId){$selectedPetTile=$tile}
$stack=New-Object Windows.Controls.StackPanel
$previewSource=Get-PetPreviewImage $entry.CachePath $entry.SourcePath $entry.Id
if($null-ne$previewSource){
$preview=New-Object Windows.Controls.Image;$preview.Width=82;$preview.Height=82
$preview.Stretch=[Windows.Media.Stretch]::Uniform;$preview.Source=$previewSource
}else{
$preview=New-Object Windows.Controls.Border;$preview.Width=82;$preview.Height=82
$preview.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(48,78,101))
$initial=New-Object Windows.Controls.TextBlock
$initial.Text=$entry.DisplayName.Substring(0,1).ToUpperInvariant();$initial.FontSize=30
$initial.FontWeight=[Windows.FontWeights]::Bold;$initial.Foreground=[Windows.Media.Brushes]::White
$initial.HorizontalAlignment=[Windows.HorizontalAlignment]::Center;$initial.VerticalAlignment=[Windows.VerticalAlignment]::Center
$preview.Child=$initial
}
$stack.Children.Add($preview)|Out-Null
$name=New-Object Windows.Controls.TextBlock;$name.Text=$entry.DisplayName
$name.Foreground=[Windows.Media.Brushes]::White;$name.FontSize=11;$name.FontWeight=[Windows.FontWeights]::SemiBold
$name.HorizontalAlignment=[Windows.HorizontalAlignment]::Center;$name.TextTrimming=[Windows.TextTrimming]::CharacterEllipsis
$stack.Children.Add($name)|Out-Null
if($entry.Id-eq$selectedPetId){
$active=New-Object Windows.Controls.TextBlock;$active.Text='ACTIVE';$active.FontSize=9
$active.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(144,210,255))
$active.HorizontalAlignment=[Windows.HorizontalAlignment]::Center;$stack.Children.Add($active)|Out-Null
}
$tile.Content=$stack
$tile.Add_Click({
param($sender,$eventArgs)
$selectedEntry=$sender.Tag
$selectedId=[string]$selectedEntry.Id
$currentId=$(if($script:petSelectorTarget-eq'P2'){$script:p2PetId}else{$script:activePetId})
if($selectedId-eq$currentId){$script:petSelectorWindow.Hide();return}
Select-PetForPlayer $selectedEntry $script:petSelectorTarget|Out-Null
$script:petSelectorWindow.Hide()
})
$script:petSelectorPanel.Children.Add($tile)|Out-Null
}
$script:petSelectorWindow.Left=[Windows.SystemParameters]::VirtualScreenLeft+[Windows.SystemParameters]::VirtualScreenWidth-$script:petSelectorWindow.Width-20
$script:petSelectorWindow.Top=$script:menuButtonWindow.Top+$script:menuButtonWindow.Height+6
if(-not$script:petSelectorWindow.IsVisible){$script:petSelectorWindow.Show()}else{$script:petSelectorWindow.Activate()|Out-Null}
if($null-ne$selectedPetTile){$script:petSelectorWindow.UpdateLayout();$selectedPetTile.BringIntoView()}
}
function Toggle-PetSelector {
if($null-ne$script:petSelectorWindow-and$script:petSelectorWindow.IsVisible){$script:petSelectorWindow.Hide()}else{Show-PetSelector}
}
function Update-Menu {
if ($null -eq $script:menuWindow) { return }
$state = $(if ($script:frozen) { 'FROZEN' } elseif($script:footballActive){'FOOTBALL'} elseif($script:wispfallActive){$(if($script:wispfallGameOver){'WISPFALL OVER'}else{'WISPFALL'})} elseif ($script:playroomActive) { 'SANDBOX' } elseif ($script:hearts -le 0) { "$($script:gameType.ToUpper()) OVER" } elseif ($script:active) { $script:gameType.ToUpper() } else { 'PET PAUSED' })
$script:menuTitle.Text = "$([string][char]0x2601) WINDOWISP | $state"
$script:menuActionButton.Content.Children[0].Text = $(if ($script:active -and -not $script:playroomActive) { "Pause $($script:gameType)" } else { 'Start Game Mode' })
$restartAvailable=($script:footballActive-or$script:wispfallActive-or($script:active-and-not$script:playroomActive))
$script:menuRestartButton.Visibility=$(if($restartAvailable){[Windows.Visibility]::Visible}else{[Windows.Visibility]::Collapsed})
$script:menuRestartButton.Content.Children[0].Text=$(if($script:footballActive){'Restart Football Match'}elseif($script:wispfallActive){'Restart Wispfall Run'}else{"Restart $($script:gameType)"})
$script:menuFreezeButton.Content.Children[0].Text = $(if ($script:frozen) { 'Unfreeze Pet' } else { 'Freeze Pet' })
$script:menuSpeedButton.Content.Children[1].Text = $script:petSpeedNames[$script:petSpeedIndex]
$script:menuWanderButton.Content.Children[1].Text = $(if ($script:autoWander) { 'On' } else { 'Off' })
$script:menuTopmostButton.Content.Children[1].Text = $(if ($script:alwaysOnTop) { 'On' } else { 'Off' })
if($null-ne$script:menuFamiliarButton){$script:menuFamiliarButton.Content.Children[1].Text=$script:familiarMode}
$script:p2MenuButton.Content.Children[0].Text = $(if ($script:twoPlayerActive) { 'Disable Two Player' } else { 'Enable Two Player' })
$script:p2PresetButton.Content.Children[1].Text = $script:p2Preset
$script:p2PresetButton.Visibility = $(if($script:twoPlayerActive){[Windows.Visibility]::Visible}else{[Windows.Visibility]::Collapsed})
if($null-ne$script:p2ControllerButton){
$script:p2ControllerButton.Content.Children[1].Text=$script:p2ControllerMode
$script:p2ControllerButton.Visibility=$(if($script:twoPlayerActive){[Windows.Visibility]::Visible}else{[Windows.Visibility]::Collapsed})
}
if($null-ne$script:p2AIDifficultyButton){
$script:p2AIDifficultyButton.Content.Children[1].Text=$script:p2AIDifficulty
$script:p2AIDifficultyButton.Visibility=$(if($script:twoPlayerActive-and$script:p2ControllerMode-eq'Opponent'){[Windows.Visibility]::Visible}else{[Windows.Visibility]::Collapsed})
}
$script:playroomButton.Content.Children[0].Text = $(if($script:wispfallActive-or$script:footballActive){'Return to Sandbox'}elseif ($script:playroomActive) { 'Open Builder Hotbar' } else { 'Start Sandbox Mode' })
if($null-ne$script:sandboxToolButton){
$script:sandboxToolButton.Visibility=[Windows.Visibility]::Visible
$script:menuButtonWindow.Width=188
$script:menuButtonWindow.Left=[Windows.SystemParameters]::VirtualScreenLeft+[Windows.SystemParameters]::VirtualScreenWidth-$script:menuButtonWindow.Width-20
}
if (-not $script:playroomActive) {
$script:playroomPanel.Visibility = [Windows.Visibility]::Collapsed
}
if ($script:active -and -not $script:playroomActive) { $script:gameModePanel.Visibility = [Windows.Visibility]::Collapsed }
}
function Toggle-Menu {
if ($null -eq $script:menuWindow) { return }
if ($script:menuWindow.IsVisible) {
if ($null -ne $script:controlsWindow) { $script:controlsWindow.Close(); $script:controlsWindow = $null }
Hide-ControlOverlay
$script:menuWindow.Hide()
} else {
Update-Menu
$left = $script:menuButtonWindow.Left - $script:menuWindow.Width + $script:menuButtonWindow.Width
$top = $script:menuButtonWindow.Top + $script:menuButtonWindow.Height + 6
$minLeft = [Windows.SystemParameters]::VirtualScreenLeft + 8
$maxLeft = [Windows.SystemParameters]::VirtualScreenLeft + [Windows.SystemParameters]::VirtualScreenWidth - $script:menuWindow.Width - 8
$maxTop = [Windows.SystemParameters]::VirtualScreenTop + [Windows.SystemParameters]::VirtualScreenHeight - $script:menuWindow.Height - 8
$script:menuWindow.Left = [Math]::Max($minLeft, [Math]::Min($maxLeft, $left))
$script:menuWindow.Top = [Math]::Min($maxTop, $top)
$script:menuWindow.Show(); $script:menuWindow.Activate() | Out-Null
}
}
function Show-QuickTutorial([bool]$firstRun = $false) {
if ($null -ne $script:tutorialWindow -and $script:tutorialWindow.IsVisible) {
$script:tutorialWindow.Activate() | Out-Null
return
}
$tutorial = New-Object Windows.Window
$tutorial.Title = 'Windowisp Quick Tutorial'
$tutorial.Width = 430; $tutorial.Height = 430
$tutorial.WindowStyle = [Windows.WindowStyle]::None
$tutorial.AllowsTransparency = $true
$tutorial.Background = [Windows.Media.Brushes]::Transparent
$tutorial.Topmost = $true; $tutorial.ShowInTaskbar = $false
$tutorial.ResizeMode = [Windows.ResizeMode]::NoResize
$tutorial.Left = [Windows.SystemParameters]::VirtualScreenLeft + ([Windows.SystemParameters]::VirtualScreenWidth - $tutorial.Width) / 2
$tutorial.Top = [Windows.SystemParameters]::VirtualScreenTop + ([Windows.SystemParameters]::VirtualScreenHeight - $tutorial.Height) / 2
$card = New-Object Windows.Controls.Border
$card.CornerRadius = [Windows.CornerRadius]::new(18)
$card.BorderThickness = [Windows.Thickness]::new(2)
$card.BorderBrush = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(74, 174, 238))
$card.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(250, 26, 23, 38))
$card.Padding = [Windows.Thickness]::new(24, 20, 24, 20)
$panel = New-Object Windows.Controls.StackPanel
$title = New-Object Windows.Controls.TextBlock
$title.Text = $(if ($firstRun) { 'WELCOME TO WINDOWISP' } else { 'WINDOWISP QUICK TUTORIAL' })
$title.Foreground = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(116, 211, 255))
$title.FontSize = 20; $title.FontWeight = [Windows.FontWeights]::Bold
$title.Margin = [Windows.Thickness]::new(0, 0, 0, 12)
$panel.Children.Add($title) | Out-Null
$intro = New-Object Windows.Controls.TextBlock
$intro.Text = 'Windowisp starts in Sandbox so you can meet your pet and build safely.'
$intro.TextWrapping = [Windows.TextWrapping]::Wrap
$intro.Foreground = [Windows.Media.Brushes]::White; $intro.FontSize = 14
$intro.Margin = [Windows.Thickness]::new(0, 0, 0, 13)
$panel.Children.Add($intro) | Out-Null
foreach ($tipText in @(
'1 BUILD - Use the blue tools button to place blocks, toys, gadgets and hazards.',
'2 PLAY - Open the paw menu, choose Start Game Mode, then Survival or Dodgeball.',
'3 MOVE - A/D moves, W or Space jumps, S drops, J attacks and E carries toys.',
'4 DESKTOP - Application windows are platforms. Try moving or minimizing one.'
)) {
$tip = New-Object Windows.Controls.TextBlock
$tip.Text = $tipText; $tip.TextWrapping = [Windows.TextWrapping]::Wrap
$tip.Foreground = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(226, 232, 244))
$tip.FontSize = 13; $tip.Margin = [Windows.Thickness]::new(0, 0, 0, 10)
$panel.Children.Add($tip) | Out-Null
}
$note = New-Object Windows.Controls.TextBlock
$note.Text = 'Wires and editing handles hide automatically during gameplay. Press Ctrl+Alt+P whenever you need the menu.'
$note.TextWrapping = [Windows.TextWrapping]::Wrap
$note.Foreground = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(175, 185, 205))
$note.FontSize = 12; $note.Margin = [Windows.Thickness]::new(0, 2, 0, 14)
$panel.Children.Add($note) | Out-Null
$done = New-Object Windows.Controls.Button
$done.Content = 'Got it - start building'
$done.Height = 38; $done.FontWeight = [Windows.FontWeights]::Bold
$done.Foreground = [Windows.Media.Brushes]::White
$done.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(231, 119, 50))
$done.BorderThickness = [Windows.Thickness]::new(0); $done.Cursor = [Windows.Input.Cursors]::Hand
$done.Add_Click({
try {
$directory = Split-Path -Parent $script:tutorialSeenPath
if (-not (Test-Path -LiteralPath $directory -PathType Container)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }
Set-Content -LiteralPath $script:tutorialSeenPath -Value 'Windowisp tutorial v1 completed' -Encoding UTF8
} catch {}
if ($null -ne $script:tutorialWindow) { $script:tutorialWindow.Close() }
})
$panel.Children.Add($done) | Out-Null
$card.Child = $panel; $tutorial.Content = $card
$tutorial.Add_Closed({ $script:tutorialWindow = $null })
$script:tutorialWindow = $tutorial
$tutorial.Show(); $tutorial.Activate() | Out-Null
}
function New-ControlCentre {
$script:menuButtonWindow = New-Object Windows.Window
$script:menuButtonWindow.Title = 'Windowisp Control Centre'
$script:menuButtonWindow.Width = 188; $script:menuButtonWindow.Height = 48
$script:menuButtonWindow.WindowStyle = [Windows.WindowStyle]::None
$script:menuButtonWindow.AllowsTransparency = $true
$script:menuButtonWindow.Background = [Windows.Media.Brushes]::Transparent
$script:menuButtonWindow.Topmost = $true; $script:menuButtonWindow.ShowInTaskbar = $false
$script:menuButtonWindow.ResizeMode = [Windows.ResizeMode]::NoResize
$script:menuButtonWindow.Left = [Windows.SystemParameters]::VirtualScreenLeft + [Windows.SystemParameters]::VirtualScreenWidth - 208
$script:menuButtonWindow.Top = [Windows.SystemParameters]::VirtualScreenTop + 110
$dock = New-Object Windows.Controls.DockPanel
$grip = New-Object Windows.Controls.Border
$grip.Width = 12
$grip.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(245,76,137,177))
$grip.CornerRadius = [Windows.CornerRadius]::new(8,0,0,8)
$grip.Cursor = [Windows.Input.Cursors]::SizeAll
$gripText = New-Object Windows.Controls.TextBlock
$gripText.Text = [string][char]0x22EE
$gripText.Foreground = [Windows.Media.Brushes]::White
$gripText.Opacity = 0.65;$gripText.FontSize = 18
$gripText.HorizontalAlignment = 'Center';$gripText.VerticalAlignment = 'Center'
$grip.Child = $gripText
$grip.Add_MouseLeftButtonDown({$script:menuButtonWindow.DragMove()})
[Windows.Controls.DockPanel]::SetDock($grip,[Windows.Controls.Dock]::Left)
$dock.Children.Add($grip)|Out-Null
$buttonGrid = New-Object Windows.Controls.Primitives.UniformGrid
$buttonGrid.Rows=1;$buttonGrid.Columns=4
$paw = New-Object Windows.Controls.Button
$paw.Focusable=$false;$paw.IsTabStop=$false
$paw.Content = ([string][char]::ConvertFromUtf32(0x1F43E))
$paw.FontFamily = New-Object Windows.Media.FontFamily('Segoe UI Emoji')
$paw.FontSize = 23; $paw.ToolTip = 'Open Windowisp menu (Ctrl+Alt+P)'
$paw.Background = New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(239,148,67),[Windows.Media.Color]::FromRgb(171,79,35),90)
$paw.Foreground = [Windows.Media.Brushes]::White; $paw.BorderThickness = [Windows.Thickness]::new(0)
$paw.Cursor = [Windows.Input.Cursors]::Hand
# Use a mouse-only event. WPF Button.Click can also be raised by Space when
# Windows restores focus to this floating widget after another window hides.
$paw.Add_PreviewMouseLeftButtonDown({
param($sender,$eventArgs)
$eventArgs.Handled=$true
Toggle-Menu
})
$buttonGrid.Children.Add($paw) | Out-Null
$script:sandboxToolButton=New-Object Windows.Controls.Button
$script:sandboxToolButton.Focusable=$false;$script:sandboxToolButton.IsTabStop=$false
$script:sandboxToolButton.Content=([string][char]::ConvertFromUtf32(0x1F6E0))
$script:sandboxToolButton.FontFamily=New-Object Windows.Media.FontFamily('Segoe UI Emoji')
$script:sandboxToolButton.FontSize=20
$script:sandboxToolButton.ToolTip='Open or close Sandbox builder hotbar'
$script:sandboxToolButton.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(94,183,108),[Windows.Media.Color]::FromRgb(40,105,66),90)
$script:sandboxToolButton.Foreground=[Windows.Media.Brushes]::White
$script:sandboxToolButton.BorderThickness=[Windows.Thickness]::new(0)
$script:sandboxToolButton.Cursor=[Windows.Input.Cursors]::Hand
$script:sandboxToolButton.Visibility=[Windows.Visibility]::Visible
$script:sandboxToolButton.Add_PreviewMouseLeftButtonDown({
param($sender,$eventArgs)
$eventArgs.Handled=$true
if(-not$script:playroomActive){Start-Playroom}
if($null-eq$script:sandboxHotbarWindow){New-SandboxToolbox}
Set-SandboxToolboxVisible (-not$script:sandboxHotbarWindow.IsVisible)
if($null-ne$script:menuWindow-and$script:menuWindow.IsVisible){$script:menuWindow.Hide()}
})
$buttonGrid.Children.Add($script:sandboxToolButton)|Out-Null
$script:petSelectorButton=New-Object Windows.Controls.Button
$script:petSelectorButton.Focusable=$false;$script:petSelectorButton.IsTabStop=$false
$script:petSelectorButton.FontFamily=New-Object Windows.Media.FontFamily('Segoe UI Emoji')
$script:petSelectorButton.FontSize=20
$script:petSelectorButton.Foreground=[Windows.Media.Brushes]::White
$script:petSelectorButton.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(157,119,218),[Windows.Media.Color]::FromRgb(82,61,139),90)
$script:petSelectorButton.BorderThickness=[Windows.Thickness]::new(0)
$script:petSelectorButton.Cursor=[Windows.Input.Cursors]::Hand
$script:petSelectorButton.Add_PreviewMouseLeftButtonDown({
param($sender,$eventArgs)
$eventArgs.Handled=$true
Toggle-PetSelector
})
$buttonGrid.Children.Add($script:petSelectorButton)|Out-Null
$script:wipPowerupButton=New-Object Windows.Controls.Button
$script:wipPowerupButton.Focusable=$false;$script:wipPowerupButton.IsTabStop=$false;$script:wipPowerupButton.Cursor=[Windows.Input.Cursors]::Hand
$script:wipPowerupButton.Tag='FuturePowerupWIP';$script:wipPowerupButton.ToolTip='Future Power-ups - work in progress'
$script:wipPowerupButton.Background=New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(222,58,72),[Windows.Media.Color]::FromRgb(122,24,42),90)
$script:wipPowerupButton.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,151,159));$script:wipPowerupButton.BorderThickness=[Windows.Thickness]::new(0)
$wipStack=New-Object Windows.Controls.StackPanel;$wipStack.VerticalAlignment='Center'
$wipQuestion=New-Object Windows.Controls.TextBlock;$wipQuestion.Text='???';$wipQuestion.FontSize=13;$wipQuestion.FontWeight='Black';$wipQuestion.Foreground=[Windows.Media.Brushes]::White;$wipQuestion.HorizontalAlignment='Center'
$wipSign=New-Object Windows.Controls.Border;$wipSign.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,226,104));$wipSign.CornerRadius=[Windows.CornerRadius]::new(2);$wipSign.Padding=[Windows.Thickness]::new(3,0,3,0);$wipSign.HorizontalAlignment='Center'
$wipText=New-Object Windows.Controls.TextBlock;$wipText.Text='WIP';$wipText.FontSize=8;$wipText.FontWeight='Bold';$wipText.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(76,39,33));$wipSign.Child=$wipText
$wipStack.Children.Add($wipQuestion)|Out-Null;$wipStack.Children.Add($wipSign)|Out-Null;$script:wipPowerupButton.Content=$wipStack
$script:wipPowerupButton.Add_PreviewMouseLeftButtonDown({param($sender,$eventArgs)$eventArgs.Handled=$true;if($null-ne$script:menuWindow-and-not$script:menuWindow.IsVisible){$script:menuWindow.Show()};$script:menuTitle.Text="$([string][char]0x2601) POWER-UPS | COMING SOON"})
$buttonGrid.Children.Add($script:wipPowerupButton)|Out-Null
$dock.Children.Add($buttonGrid)|Out-Null
$script:menuButtonWindow.Content = $dock
Update-PetSelectorButton
$script:menuWindow = New-Object Windows.Window
$script:menuWindow.Title = 'Windowisp Menu'
$script:menuWindow.Width = 300; $script:menuWindow.Height = 470
$script:menuWindow.WindowStyle = [Windows.WindowStyle]::None
$script:menuWindow.AllowsTransparency = $true
$script:menuWindow.Background = New-Object Windows.Media.LinearGradientBrush ([Windows.Media.Color]::FromRgb(29,62,90),[Windows.Media.Color]::FromRgb(15,32,51),90)
$script:menuWindow.Topmost = $true; $script:menuWindow.ShowInTaskbar = $false
$script:menuWindow.ResizeMode = [Windows.ResizeMode]::NoResize
$panel = New-Object Windows.Controls.StackPanel
$script:menuTitle = New-Object Windows.Controls.TextBlock
$script:menuTitle.Foreground = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(201,236,253)); $script:menuTitle.FontSize = 15
$script:menuTitle.FontWeight = [Windows.FontWeights]::Bold
$script:menuTitle.Margin = [Windows.Thickness]::new(16, 14, 12, 10)
$panel.Children.Add($script:menuTitle) | Out-Null
$script:menuActionButton = New-MenuButton 'Start Game Mode' {
if ($script:active -and -not $script:playroomActive) {
Set-GameMode $false
} else {
$script:gameModePanel.Visibility = $(if ($script:gameModePanel.Visibility -eq [Windows.Visibility]::Visible) { [Windows.Visibility]::Collapsed } else { [Windows.Visibility]::Visible })
}
Update-Menu
} 'F8'
$panel.Children.Add($script:menuActionButton) | Out-Null
$script:gameModePanel = New-Object Windows.Controls.StackPanel
$script:gameModePanel.Margin = [Windows.Thickness]::new(8, 0, 8, 2)
$script:gameModePanel.Visibility = [Windows.Visibility]::Collapsed
$script:gameModePanel.Children.Add((New-MenuButton 'Survival Mode' { Start-GameType 'Survival' })) | Out-Null
$script:gameModePanel.Children.Add((New-MenuButton 'Dodgeball' { Start-GameType 'Dodgeball' } '3 hits')) | Out-Null
$script:gameModePanel.Children.Add((New-MenuButton 'Football' { Start-GameType 'Football' } 'First to 3')) | Out-Null
$script:gameModePanel.Children.Add((New-MenuButton 'Wispfall' { Start-GameType 'Wispfall' } 'Climb!')) | Out-Null
$panel.Children.Add($script:gameModePanel) | Out-Null
$script:menuRestartButton = New-MenuButton 'Restart Round' { Restart-CurrentGame } 'F9'
$panel.Children.Add($script:menuRestartButton) | Out-Null
$script:menuFreezeButton = New-MenuButton 'Freeze Pet' { Set-Frozen (-not $script:frozen) } 'F7'
$panel.Children.Add($script:menuFreezeButton) | Out-Null
$script:p2MenuButton = New-MenuButton 'Enable Two Player' { Set-TwoPlayerMode (-not $script:twoPlayerActive) }
$panel.Children.Add($script:p2MenuButton) | Out-Null
$script:p2PresetButton = New-MenuButton 'Player 2 Controls' {
$script:p2Preset = $(if ($script:p2Preset -eq 'Numpad') { 'Laptop' } else { 'Numpad' })
Set-Player2PresetBindings $script:p2Preset
Update-Menu
} 'Numpad'
$panel.Children.Add($script:p2PresetButton) | Out-Null
$script:p2ControllerButton=New-MenuButton 'Player 2 Controller' {
$next=$(switch($script:p2ControllerMode){'Human'{'Opponent'}'Opponent'{'Friend'}default{'Human'}})
Set-Player2Controller $next
} 'Human'
$panel.Children.Add($script:p2ControllerButton)|Out-Null
$script:p2AIDifficultyButton=New-MenuButton 'Opponent Difficulty' {
$next=$(switch($script:p2AIDifficulty){'Friendly'{'Normal'}'Normal'{'Tricky'}default{'Friendly'}})
Set-Player2AIDifficulty $next
} 'Normal'
$panel.Children.Add($script:p2AIDifficultyButton)|Out-Null
$script:playroomButton = New-MenuButton 'Start Sandbox Mode' {
if($script:wispfallActive-or$script:footballActive){
Return-ToSandbox
}elseif ($script:playroomActive) {
if($null-eq$script:sandboxHotbarWindow){New-SandboxToolbox}
Set-SandboxToolboxVisible $true
} else {
Start-Playroom
Set-SandboxToolboxVisible $true
}
if($null-ne$script:menuWindow){$script:menuWindow.Hide()}
}
$panel.Children.Add($script:playroomButton) | Out-Null
$script:playroomPanel = New-Object Windows.Controls.StackPanel
$script:playroomPanel.Margin = [Windows.Thickness]::new(8, 0, 8, 2)
$script:playroomPanel.Visibility = [Windows.Visibility]::Collapsed
$script:playroomPanel.Children.Add((New-MenuButton 'Spawn Ball' { Add-PlayroomBall })) | Out-Null
$script:playroomPanel.Children.Add((New-MenuButton 'Place Normal Block' { Add-PlayroomBlock 'Normal' })) | Out-Null
$script:playroomPanel.Children.Add((New-MenuButton 'Place Wooden Platform' { Add-PlayroomBlock 'Wooden' })) | Out-Null
$script:playroomPanel.Children.Add((New-MenuButton 'Place Bouncy Block' { Add-PlayroomBlock 'Bouncy' })) | Out-Null
$script:playroomPanel.Children.Add((New-MenuButton 'Place Fire Block' { Add-PlayroomBlock 'Fire' })) | Out-Null
$script:playroomPanel.Children.Add((New-MenuButton 'Place Ice Block' { Add-PlayroomBlock 'Ice' })) | Out-Null
$script:playroomPanel.Children.Add((New-MenuButton 'Place Ladder' { Add-PlayroomBlock 'Ladder' })) | Out-Null
$script:playroomPanel.Children.Add((New-MenuButton 'Place Conveyor' { Add-PlayroomBlock 'Conveyor' })) | Out-Null
$script:playroomPanel.Children.Add((New-MenuButton 'Clear Toys + Blocks' { Clear-PlayroomObjects })) | Out-Null
$script:playroomPanel.Children.Add((New-MenuButton 'Exit Sandbox (Keep Blocks)' { Stop-Playroom })) | Out-Null
$panel.Children.Add($script:playroomPanel) | Out-Null
$panel.Children.Add((New-MenuButton 'Reset Position' { Reset-PetPosition })) | Out-Null
$panel.Children.Add((New-MenuButton 'Controls' { Show-ControlOverlay } 'Edit')) | Out-Null
$panel.Children.Add((New-MenuButton 'Quick Tutorial' { Show-QuickTutorial $false } 'Help')) | Out-Null
$settingsButton = New-MenuButton 'Settings' {
$script:settingsPanel.Visibility = $(if ($script:settingsPanel.Visibility -eq [Windows.Visibility]::Visible) { [Windows.Visibility]::Collapsed } else { [Windows.Visibility]::Visible })
}
$panel.Children.Add($settingsButton) | Out-Null
$script:settingsPanel = New-Object Windows.Controls.StackPanel
$script:settingsPanel.Margin = [Windows.Thickness]::new(8, 0, 8, 2)
$script:settingsPanel.Visibility = [Windows.Visibility]::Collapsed
$script:menuSpeedButton = New-MenuButton 'Pet Speed' {
$script:petSpeedIndex = ($script:petSpeedIndex + 1) % $script:petSpeedNames.Count
Update-Menu
} $script:petSpeedNames[$script:petSpeedIndex]
$script:settingsPanel.Children.Add($script:menuSpeedButton) | Out-Null
$script:menuWanderButton = New-MenuButton 'Auto Wander' {
$script:autoWander = -not $script:autoWander
Update-Menu
} 'Off'
$script:settingsPanel.Children.Add($script:menuWanderButton) | Out-Null
$script:menuTopmostButton = New-MenuButton 'Always on Top' {
Set-AlwaysOnTop (-not $script:alwaysOnTop)
Update-Menu
} 'On'
$script:settingsPanel.Children.Add($script:menuTopmostButton) | Out-Null
$script:menuFamiliarButton=New-MenuButton 'Familiar Speech' {
$modes=@('Quiet','Normal','Chatty');$current=[Array]::IndexOf($modes,$script:familiarMode);Set-FamiliarMode $modes[(($current+1)%$modes.Count)]
} $script:familiarMode
$script:settingsPanel.Children.Add($script:menuFamiliarButton)|Out-Null
$panel.Children.Add($script:settingsPanel) | Out-Null
$recoveryButton = New-MenuButton 'Recovery' {
$script:recoveryPanel.Visibility = $(if ($script:recoveryPanel.Visibility -eq [Windows.Visibility]::Visible) { [Windows.Visibility]::Collapsed } else { [Windows.Visibility]::Visible })
} 'Stuck?'
$panel.Children.Add($recoveryButton) | Out-Null
$script:recoveryPanel = New-Object Windows.Controls.StackPanel
$script:recoveryPanel.Margin = [Windows.Thickness]::new(8, 0, 8, 2)
$script:recoveryPanel.Visibility = [Windows.Visibility]::Collapsed
$script:recoveryPanel.Children.Add((New-MenuButton 'Unstick Pet' { Reset-WindowispRuntime } 'Keeps builds')) | Out-Null
$script:recoveryPanel.Children.Add((New-MenuButton 'Restart Windowisp' { Restart-WindowispRuntime } 'Full reset')) | Out-Null
$panel.Children.Add($script:recoveryPanel) | Out-Null
$creditsButton = New-MenuButton 'Credits & Contact' {
$script:creditsPanel.Visibility = $(if ($script:creditsPanel.Visibility -eq [Windows.Visibility]::Visible) { [Windows.Visibility]::Collapsed } else { [Windows.Visibility]::Visible })
} 'About'
$panel.Children.Add($creditsButton) | Out-Null
$script:creditsPanel = New-Object Windows.Controls.StackPanel
$script:creditsPanel.Margin = [Windows.Thickness]::new(8, 0, 8, 4)
$script:creditsPanel.Visibility = [Windows.Visibility]::Collapsed
$creditsCard=New-Object Windows.Controls.Border
$creditsCard.Margin=[Windows.Thickness]::new(4,2,4,4);$creditsCard.Padding=[Windows.Thickness]::new(12,10,12,10)
$creditsCard.CornerRadius=[Windows.CornerRadius]::new(7)
$creditsCard.Background=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(20,43,67))
$creditsCard.BorderBrush=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(104,168,211));$creditsCard.BorderThickness=[Windows.Thickness]::new(1)
$creditsTextPanel=New-Object Windows.Controls.StackPanel
$creditsTitle=New-Object Windows.Controls.TextBlock;$creditsTitle.Text='WINDOWISP 0.3.0';$creditsTitle.FontWeight='Bold';$creditsTitle.FontSize=14;$creditsTitle.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(201,236,253));$creditsTextPanel.Children.Add($creditsTitle)|Out-Null
$creditsByline=New-Object Windows.Controls.TextBlock;$creditsByline.Text='Designed and created by CAIUTO AI';$creditsByline.Margin=[Windows.Thickness]::new(0,3,0,0);$creditsByline.Foreground=[Windows.Media.Brushes]::White;$creditsTextPanel.Children.Add($creditsByline)|Out-Null
$creditsBuild=New-Object Windows.Controls.TextBlock;$creditsBuild.Text='Developed with OpenAI Codex';$creditsBuild.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(154,184,205));$creditsBuild.FontSize=11;$creditsTextPanel.Children.Add($creditsBuild)|Out-Null
$creditsLicence=New-Object Windows.Controls.TextBlock;$creditsLicence.Text='Free beta - MIT licence';$creditsLicence.Margin=[Windows.Thickness]::new(0,5,0,0);$creditsLicence.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(154,184,205));$creditsLicence.FontSize=11;$creditsTextPanel.Children.Add($creditsLicence)|Out-Null
$creditsContact=New-Object Windows.Controls.TextBlock;$creditsContact.Text='Support: cai-lear@hotmail.com';$creditsContact.Margin=[Windows.Thickness]::new(0,5,0,0);$creditsContact.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,211,118));$creditsContact.FontSize=11;$creditsContact.TextWrapping='Wrap';$creditsTextPanel.Children.Add($creditsContact)|Out-Null
$creditsCard.Child=$creditsTextPanel;$script:creditsPanel.Children.Add($creditsCard)|Out-Null
$script:creditsPanel.Children.Add((New-MenuButton 'Visit Windowisp Website' { Start-Process 'https://caiuto-ai.caiuto.chatgpt.site' } 'Open'))|Out-Null
$script:creditsPanel.Children.Add((New-MenuButton 'Email Support' { Start-Process 'mailto:cai-lear@hotmail.com?subject=Windowisp%20Support' } 'Contact'))|Out-Null
$script:creditsPanel.Children.Add((New-MenuButton 'Copy Support Email' { try{[Windows.Clipboard]::SetText('cai-lear@hotmail.com')}catch{} } 'Copy'))|Out-Null
$script:creditsPanel.Children.Add((New-MenuButton 'Third-Party Notices' { $notices=Join-Path $PSScriptRoot 'THIRD_PARTY_NOTICES.md';if(Test-Path -LiteralPath $notices){Start-Process $notices} } 'Credits'))|Out-Null
$panel.Children.Add($script:creditsPanel) | Out-Null
$panel.Children.Add((New-MenuButton 'Close Windowisp' { $window.Close() } 'F10')) | Out-Null
$scroll = New-Object Windows.Controls.ScrollViewer
$scroll.VerticalScrollBarVisibility = [Windows.Controls.ScrollBarVisibility]::Auto
$scroll.HorizontalScrollBarVisibility = [Windows.Controls.ScrollBarVisibility]::Disabled
$scroll.Content = $panel
$menuRoot = New-Object Windows.Controls.Grid
$menuRoot.Children.Add($scroll)|Out-Null
$menuRoot.Children.Add((New-ControlsOverlay))|Out-Null
$script:menuWindow.Content = $menuRoot
Update-Menu
$script:menuButtonWindow.Show()
}
function New-PlayerStatusWindow([string]$player, [bool]$tinted) {
$statusWindow = New-Object Windows.Window
$statusWindow.Title = "Windowisp $player Status"
$statusWindow.Width = 205; $statusWindow.Height = 54
$statusWindow.WindowStyle = [Windows.WindowStyle]::None; $statusWindow.AllowsTransparency = $true
$statusWindow.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(225, 24, 21, 36))
$statusWindow.Topmost = $true; $statusWindow.ShowInTaskbar = $false; $statusWindow.ShowActivated = $false
$grid = New-Object Windows.Controls.Grid
$portrait = New-Object Windows.Controls.Image
$portrait.Width = 45; $portrait.Height = 48; $portrait.HorizontalAlignment = 'Left'; $portrait.Margin = [Windows.Thickness]::new(5,2,0,2)
$portrait.Source = $(if($player-eq'P2'){Get-P2SpriteFrame 0 0}else{Get-PetSpriteFrame 0 0})
if($tinted){$portrait.Effect=New-Object Windows.Media.Effects.DropShadowEffect;$portrait.Effect.Color=[Windows.Media.Color]::FromRgb(236,67,190);$portrait.Effect.BlurRadius=12;$portrait.Effect.ShadowDepth=0}
$text = New-Object Windows.Controls.TextBlock
$text.Margin=[Windows.Thickness]::new(55,0,8,0);$text.VerticalAlignment='Center';$text.FontFamily=New-Object Windows.Media.FontFamily('Segoe UI Emoji')
$text.FontSize=18;$text.FontWeight=[Windows.FontWeights]::Bold;$text.Foreground=New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255,76,99))
$grid.Children.Add($portrait)|Out-Null;$grid.Children.Add($text)|Out-Null;$statusWindow.Content=$grid
$statusWindow.Show();Set-OverlayClickThrough $statusWindow;$statusWindow.Visibility=[Windows.Visibility]::Hidden
return [pscustomobject]@{Window=$statusWindow;Text=$text;Portrait=$portrait}
}
function New-HudWindows {
$script:hudWindow = New-Object Windows.Window
$script:hudWindow.Title = 'Windowisp HUD'
$script:hudWindow.Width = 570; $script:hudWindow.Height = 70
$script:hudWindow.WindowStyle = [Windows.WindowStyle]::None; $script:hudWindow.AllowsTransparency = $true
$script:hudWindow.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(205, 20, 18, 31))
$script:hudWindow.Topmost = $true; $script:hudWindow.ShowInTaskbar = $false; $script:hudWindow.ShowActivated = $false
$script:hudText = New-Object Windows.Controls.TextBlock
$script:hudText.Foreground = [Windows.Media.Brushes]::White; $script:hudText.FontSize = 15
$script:hudText.FontWeight = [Windows.FontWeights]::Bold; $script:hudText.TextAlignment = 'Center'
$script:hudText.VerticalAlignment = 'Center'; $script:hudWindow.Content = $script:hudText
$script:hudWindow.Left = [Windows.SystemParameters]::VirtualScreenLeft + (([Windows.SystemParameters]::VirtualScreenWidth - $script:hudWindow.Width) / 2)
$script:hudWindow.Top = [Windows.SystemParameters]::VirtualScreenTop + 12
$script:hudWindow.Show(); Set-OverlayClickThrough $script:hudWindow
$script:heartWindow = New-Object Windows.Window
$script:heartWindow.Title = 'Windowisp Hearts'
$script:heartWindow.Width = 116; $script:heartWindow.Height = 34
$script:heartWindow.WindowStyle = [Windows.WindowStyle]::None; $script:heartWindow.AllowsTransparency = $true
$script:heartWindow.Background = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(190, 28, 20, 35))
$script:heartWindow.Topmost = $true; $script:heartWindow.ShowInTaskbar = $false; $script:heartWindow.ShowActivated = $false
$script:heartText = New-Object Windows.Controls.TextBlock
$script:heartText.Foreground = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromRgb(255, 70, 92))
$script:heartText.FontFamily = New-Object Windows.Media.FontFamily('Segoe UI Emoji')
$script:heartText.FontSize = 21; $script:heartText.FontWeight = [Windows.FontWeights]::Bold
$script:heartText.TextAlignment = 'Center'; $script:heartText.VerticalAlignment = 'Center'
$script:heartWindow.Content = $script:heartText
$script:heartWindow.Show(); Set-OverlayClickThrough $script:heartWindow
$p1Status=New-PlayerStatusWindow 'P1' $false;$script:p1StatusWindow=$p1Status.Window;$script:p1StatusText=$p1Status.Text;$script:p1StatusPortrait=$p1Status.Portrait
$p2Status=New-PlayerStatusWindow 'P2' $true;$script:p2StatusWindow=$p2Status.Window;$script:p2StatusText=$p2Status.Text;$script:p2StatusPortrait=$p2Status.Portrait
}
function Ensure-ShieldWindow {
if ($null -ne $script:shieldElement) { return }
$shieldCanvas = New-Object Windows.Controls.Canvas
$shieldCanvas.Width=93;$shieldCanvas.Height=93;$shieldCanvas.IsHitTestVisible=$false
$script:shieldRing = New-Object Windows.Shapes.Ellipse
$script:shieldRing.Width = 83; $script:shieldRing.Height = 83
$script:shieldRing.Stroke = New-Object Windows.Media.SolidColorBrush ([Windows.Media.Color]::FromArgb(210, 92, 232, 255))
$script:shieldRing.StrokeThickness = 4
$script:shieldRing.StrokeDashArray = New-Object Windows.Media.DoubleCollection
@(1.2, 1.1, 3.2, 1.1) | ForEach-Object { $script:shieldRing.StrokeDashArray.Add($_) }
$script:shieldRing.RenderTransformOrigin = [Windows.Point]::new(0.5, 0.5)
$script:shieldRotate=[Windows.Media.RotateTransform]::new(0);$script:shieldRing.RenderTransform=$script:shieldRotate
$script:shieldRing.Effect = New-Object Windows.Media.Effects.DropShadowEffect
$script:shieldRing.Effect.Color = [Windows.Media.Color]::FromRgb(40, 215, 255)
$script:shieldRing.Effect.BlurRadius = 16; $script:shieldRing.Effect.ShadowDepth = 0; $script:shieldRing.Effect.Opacity = 0.9
[Windows.Controls.Canvas]::SetLeft($script:shieldRing, 5); [Windows.Controls.Canvas]::SetTop($script:shieldRing, 5)
$shieldCanvas.Children.Add($script:shieldRing) | Out-Null
$script:shieldElement=$shieldCanvas
Add-SharedGameplayElement $shieldCanvas ($script:x+(($PetWidth-93)/2)) ($script:y+(($PetHeight-93)/2))
}
function Update-Shield {
if ($script:shieldTicks -gt 0 -and $script:active -and $script:hearts -gt 0) {
Ensure-ShieldWindow
$script:shieldTicks--
$script:shieldAngle = ($script:shieldAngle + 6) % 360
$script:shieldRotate.Angle=$script:shieldAngle
Set-SharedOverlayElementPosition $script:shieldElement ($script:x+(($PetWidth-93)/2)) ($script:y+(($PetHeight-93)/2))
$script:shieldElement.Visibility = [Windows.Visibility]::Visible
} elseif ($null -ne $script:shieldElement) {
$script:shieldElement.Visibility = [Windows.Visibility]::Hidden
}
}
function Remove-Shield {
if ($null -ne $script:shieldElement) { Remove-SharedGameplayElement $script:shieldElement;$script:shieldElement=$null;$script:shieldRing=$null;$script:shieldRotate=$null }
}
function New-ShieldPickup {
if ($null -ne $script:shieldPickup) { return }
$pickupWindow = New-Object Windows.Window
$pickupWindow.Title = 'Windowisp Shield Pickup'
$pickupWindow.Width = 84; $pickupWindow.Height = 90
$pickupWindow.WindowStyle = [Windows.WindowStyle]::None; $pickupWindow.AllowsTransparency = $true
$pickupWindow.Background = [Windows.Media.Brushes]::Transparent; $pickupWindow.Topmost = $true
$pickupWindow.ShowInTaskbar = $false; $pickupWindow.ShowActivated = $false
$pickupWindow.ResizeMode = [Windows.ResizeMode]::NoResize
$pickupCanvas = New-Object Windows.Controls.Canvas
$pickupCanvas.Width=84;$pickupCanvas.Height=90;$pickupCanvas.IsHitTestVisible=$false
$shieldShape = New-Object Windows.Controls.Image
$shieldShape.Source=Get-ToyboxImageSource 'ShieldPickup';$shieldShape.Width=84;$shieldShape.Height=90;$shieldShape.Stretch='Uniform'
$shieldShape.RenderTransformOrigin = [Windows.Point]::new(0.5, 0.5)
$transformGroup = New-Object Windows.Media.TransformGroup
$scaleTransform = [Windows.Media.ScaleTransform]::new(1, 1)
$rotateTransform = [Windows.Media.RotateTransform]::new(0)
$transformGroup.Children.Add($scaleTransform); $transformGroup.Children.Add($rotateTransform)
$shieldShape.RenderTransform = $transformGroup
$pickupCanvas.Children.Add($shieldShape) | Out-Null
$left = [int][Windows.SystemParameters]::VirtualScreenLeft
$top = [int][Windows.SystemParameters]::VirtualScreenTop
$maxX = [int]($left + [Windows.SystemParameters]::VirtualScreenWidth - 120)
$minY = $top + 95; $maxY = [int]($top + [Windows.SystemParameters]::VirtualScreenHeight - 195)
$pickupX = $left + 20; $pickupY = $minY
foreach ($attempt in 1..12) {
$pickupX = $script:random.Next($left + 20, [Math]::Max($left + 21, $maxX))
$pickupY = $script:random.Next($minY, [Math]::Max($minY + 1, $maxY))
if ([Math]::Abs($pickupX - $script:x) + [Math]::Abs($pickupY - $script:y) -gt 260) { break }
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_AUTOCOLLECT_SHIELD -eq '1') {
$pickupX = [int]$script:x; $pickupY = [int]$script:y
}
Add-SharedGameplayElement $pickupCanvas $pickupX $pickupY
$script:shieldPickup = [pscustomobject]@{
Window=$null;Element=$pickupCanvas; Shape=$shieldShape; Scale=$scaleTransform; Rotate=$rotateTransform
X=[double]$pickupX; Y=[double]$pickupY; BaseY=[double]$pickupY; Angle=0.0
}
}
function Update-ShieldPickup {
if ($null -eq $script:shieldPickup) { return }
$script:shieldPickup.Angle = ($script:shieldPickup.Angle + 5) % 360
$radians = ($script:shieldPickup.Angle * [Math]::PI) / 180
$script:shieldPickup.Scale.ScaleX = [Math]::Max(0.18, [Math]::Abs([Math]::Cos($radians)))
$script:shieldPickup.Rotate.Angle = [Math]::Sin($radians) * 7
$script:shieldPickup.Y = $script:shieldPickup.BaseY + ([Math]::Sin($radians * 0.7) * 8)
Set-SharedOverlayElementPosition $script:shieldPickup.Element $script:shieldPickup.X $script:shieldPickup.Y
}
function Remove-ShieldPickup {
if ($null -ne $script:shieldPickup) { Remove-SharedGameplayElement $script:shieldPickup.Element;$script:shieldPickup = $null }
}
function Update-Hud {
if ($null -eq $script:hudWindow) { return }
$seconds = [Math]::Floor($script:elapsedTicks / 60)
$minutes = [Math]::Floor($seconds / 60); $remaining = $seconds % 60
$dashStatus = $(if ($script:dashCooldown -le 0) { 'DASH READY' } else { 'DASH {0:0.0}s' -f ($script:dashCooldown / 60) })
$shieldStatus = $(if ($script:shieldTicks -gt 0) { 'SHIELD {0:0.0}s' -f ($script:shieldTicks / 60) } else { 'SHIELD --' })
$chargeStatus = $(if ($script:charging) { 'CHARGE {0:0}%' -f ([Math]::Min(100, ($script:chargeTicks / 90) * 100)) } else { 'HOLD J' })
$script:hudText.Text = $(if($script:gameType-eq'Wispfall'){
if($script:wispfallGameOver){"WISPFALL OVER | SCORE $($script:wispfallScore) | F9 TO CLIMB AGAIN"}
else{"WISPFALL | SCORE $($script:wispfallScore) | KEEP CLIMBING!"}
}elseif($script:gameType-eq'Football'){
if($script:footballMatchOver){"FOOTBALL $($script:footballMessage) | P1 $($script:footballP1Score) - $($script:footballP2Score) P2"}
elseif($script:footballCountdownTicks-gt0){
$count=[Math]::Max(1,[Math]::Ceiling($script:footballCountdownTicks/60.0))
"FOOTBALL $($script:footballMessage) | P1 $($script:footballP1Score) - $($script:footballP2Score) P2 | $count"
}elseif($script:footballKickoffTextTicks-gt0){"FOOTBALL P1 $($script:footballP1Score) - $($script:footballP2Score) P2 | KICK OFF!"}
else{"FOOTBALL P1 $($script:footballP1Score) - $($script:footballP2Score) P2 | FIRST TO 3 | F9 RESTART"}
}elseif ($script:hearts -le 0) {
"$($script:gameType.ToUpper()) OVER TIME $('{0:00}:{1:00}' -f $minutes,$remaining)"
} elseif ($script:gameType -eq 'Dodgeball') {
'DODGEBALL TIME {0:00}:{1:00} | BALLS {2} | 3 HITS AND YOU ARE OUT' -f $minutes,$remaining,$script:dodgeballs.Count
} else {
$wave=Get-SurvivalWave;$bossText=$(if(@($script:enemies|Where-Object{$_.Kind-eq'Boss'}).Count){' | BOSS'}else{''})
'WAVE {0} SCORE {1:000000} TIME {2:00}:{3:00}{4}{5} | {6} | {7}{8}' -f $wave,$script:score,$minutes,$remaining,[Environment]::NewLine,$dashStatus,$shieldStatus,$chargeStatus,$bossText
})
$heartEmoji = ([string][char]0x2764) + ([string][char]0xFE0F)
$heartSeparator = [string][char]0x2009
$script:heartText.Text = $(if ($script:hearts -gt 0) { (($heartEmoji + $heartSeparator) * $script:hearts).Trim() } else { 'KO' })
$script:heartWindow.Left = $script:x + $PetWidth - 12
$script:heartWindow.Top = $script:y - 30
$visibility = $(if (($script:footballActive-or$script:wispfallActive-or-not$script:playroomActive)-and($script:active-or$script:hearts-le0-or$script:footballMatchOver-or$script:wispfallGameOver)) { [Windows.Visibility]::Visible } else { [Windows.Visibility]::Hidden })
$script:hudWindow.Visibility = $visibility
$script:heartWindow.Visibility=[Windows.Visibility]::Hidden
$script:p1StatusText.Text="P1 "+$(if($script:hearts-gt0){(($heartEmoji+$heartSeparator)*$script:hearts).Trim()}else{'KO'})
$script:p1StatusWindow.Left=[Windows.SystemParameters]::VirtualScreenLeft+18;$script:p1StatusWindow.Top=[Windows.SystemParameters]::VirtualScreenTop+16
$script:p1StatusWindow.Visibility=$visibility
if($script:twoPlayerActive){
$script:p2StatusText.Text="P2 "+$(if($script:p2Hearts-gt0){(($heartEmoji+$heartSeparator)*$script:p2Hearts).Trim()}else{'KO'})
$script:p2StatusWindow.Left=[Windows.SystemParameters]::VirtualScreenLeft+[Windows.SystemParameters]::VirtualScreenWidth-$script:p2StatusWindow.Width-18;$script:p2StatusWindow.Top=[Windows.SystemParameters]::VirtualScreenTop+16
$script:p2StatusWindow.Visibility=$visibility
}else{
$script:p2StatusWindow.Visibility=[Windows.Visibility]::Hidden
}
}
function Set-EnemyFrame($enemyObject, [int]$frame) {
if($null-eq$enemyObject-or$enemyObject.Frame-eq$frame-or-not$script:enemyAtlases.ContainsKey($enemyObject.Type)){return}
$enemyAtlas=$script:enemyAtlases[$enemyObject.Type]
$cellWidth=[int]($enemyAtlas.PixelWidth/4);$key="$($enemyObject.Type)|$frame"
if(-not$script:enemyFrameCache.ContainsKey($key)){
$crop=[Windows.Media.Imaging.CroppedBitmap]::new($enemyAtlas,[Windows.Int32Rect]::new(($frame*$cellWidth),0,$cellWidth,$enemyAtlas.PixelHeight))
$crop.Freeze();$script:enemyFrameCache[$key]=$crop
}
$enemyObject.Image.Source=$script:enemyFrameCache[$key]
$enemyObject.Frame = $frame
}
function Get-SurvivalWave {[Math]::Max(1,1+[Math]::Floor($script:elapsedTicks/900))}
function Get-NextEnemyType {
$wave=Get-SurvivalWave
if($wave-ge5-and($wave%5)-eq0-and$script:lastBossWave-ne$wave-and-not@($script:enemies|Where-Object{$_.Kind-eq'Boss'}).Count){
$script:lastBossWave=$wave;return 'GloamGolem'
}
$available=@('EmberImp')
if($wave-ge2){$available+=@('CinderMoth','MossHopper')}
if($wave-ge3){$available+='ShellbackRoller'}
if($wave-ge4){$available+='StormJelly'}
return $available[$script:random.Next(0,$available.Count)]
}
function New-Enemy([string]$type='') {
if([string]::IsNullOrWhiteSpace($type)){$type=Get-NextEnemyType}
if(-not$script:enemyAtlases.ContainsKey($type)-or-not$script:enemyTypes.ContainsKey($type)){return}
$definition=$script:enemyTypes[$type];$width=[double]$definition.Width;$height=[double]$definition.Height
$enemyImage = New-Object Windows.Controls.Image
$enemyImage.Width=$width;$enemyImage.Height=$height;$enemyImage.IsHitTestVisible=$false
$enemyImage.Stretch = [Windows.Media.Stretch]::Uniform
$enemyImage.RenderTransformOrigin=[Windows.Point]::new(.5,.5)
$enemyScale=[Windows.Media.ScaleTransform]::new(1,1);$enemyImage.RenderTransform=$enemyScale
$fromRight = ($script:random.Next(0, 2) -eq 1)
$left=[Windows.SystemParameters]::VirtualScreenLeft;$top=[Windows.SystemParameters]::VirtualScreenTop
$screenWidth=[Windows.SystemParameters]::VirtualScreenWidth;$screenHeight=[Windows.SystemParameters]::VirtualScreenHeight
$enemyX=$(if($fromRight){$left+$screenWidth-$width-55}else{$left+55})
$isGround=$definition.Kind-in@('Hopper','Roller','Boss')
$groundY=$top+$screenHeight-$height-48
$enemyY=$(if($isGround){$top+45+($script:enemies.Count%3)*35}else{$top+100+($script:enemies.Count%4)*115})
Add-SharedGameplayElement $enemyImage $enemyX $enemyY
$enemyObject = [pscustomobject]@{
Window=$null;Element=$enemyImage;Image=$enemyImage;Type=$type;Kind=$definition.Kind
Width=$width;Height=$height;X=[double]$enemyX;Y=[double]$enemyY;GroundY=[double]$groundY;Grounded=$false
VX=$(if($fromRight){-1.5}else{1.5});VY=0.0;Health=[int]$definition.Health;MaxHealth=[int]$definition.Health
Speed=[double]$definition.Speed;ScoreValue=[int]$definition.Score;AttackCooldown=(75+($script:enemies.Count*23));AttackTicks=0
ContactCooldown=0;Frame=-1;HomeY=[double]$enemyY;Phase=($script:random.NextDouble()*6.28);VisualScale=$enemyScale
}
$script:enemies.Add($enemyObject) | Out-Null
Set-EnemyFrame $enemyObject 0
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){Write-Output "diagnostic:enemy-spawn type=$type wave=$(Get-SurvivalWave) side=$(if($fromRight){'right'}else{'left'}) count=$($script:enemies.Count)"}
}
function Resolve-EnemyGround($enemyObject,[double]$oldY){
$oldBottom=$oldY+$enemyObject.Height;$newBottom=$enemyObject.Y+$enemyObject.Height
$floor=[Windows.SystemParameters]::VirtualScreenTop+[Windows.SystemParameters]::VirtualScreenHeight
$landing=[double]$floor
if($enemyObject.VY-ge0){
$bodyLeft=$enemyObject.X+10;$bodyRight=$enemyObject.X+$enemyObject.Width-10
for($platformIndex=0;$platformIndex-lt@($script:platforms).Count;$platformIndex++){
$platform=$script:platforms[$platformIndex]
if($bodyRight-gt$platform.Left-and$bodyLeft-lt$platform.Right-and$oldBottom-le$platform.Top+7-and$newBottom-ge$platform.Top-and$platform.Top-lt$landing-and(Test-PlatformTopExposed $platformIndex $bodyLeft $bodyRight $platform.Top)){$landing=$platform.Top}
}
foreach($block in @($script:playroomBlocks)){
if($block.Type-eq'Ladder'-or$block.TemporaryState-eq'Gone'-or[Math]::Abs($block.Angle)-gt.01){continue}
if($bodyRight-gt$block.X-and$bodyLeft-lt$block.X+$block.Width-and$oldBottom-le$block.Y+7-and$newBottom-ge$block.Y-and$block.Y-lt$landing){$landing=$block.Y}
}
}
if($newBottom-ge$landing){$enemyObject.Y=$landing-$enemyObject.Height;$enemyObject.VY=0;$enemyObject.Grounded=$true}
else{$enemyObject.Grounded=$false}
}
function Test-EnemyObstacleAhead($enemyObject,[int]$direction){
if($direction-eq0){return $false}
$probeX=$(if($direction-lt0){$enemyObject.X-12}else{$enemyObject.X+$enemyObject.Width+12})
$bodyTop=$enemyObject.Y+12;$bodyBottom=$enemyObject.Y+$enemyObject.Height-6
foreach($block in @($script:playroomBlocks)){
if($block.Type-eq'Ladder'-or$block.TemporaryState-eq'Gone'-or[Math]::Abs($block.Angle)-gt.01){continue}
if($probeX-ge$block.X-and$probeX-le$block.X+$block.Width-and$bodyBottom-gt$block.Y+5-and$bodyTop-lt$block.Y+$block.Height){return $true}
}
return $false
}
function Update-OneEnemy($enemyObject){
$targetX=$script:x+($PetWidth/2);$targetY=$script:y+($PetHeight/2)
if($script:twoPlayerActive-and$script:p2Hearts-gt0){
$p1Distance=[Math]::Abs($targetX-($enemyObject.X+$enemyObject.Width/2))
$p2Distance=[Math]::Abs(($script:p2X+$PetWidth/2)-($enemyObject.X+$enemyObject.Width/2))
if($p2Distance-lt$p1Distance){$targetX=$script:p2X+($PetWidth/2);$targetY=$script:p2Y+($PetHeight/2)}
}
$dx=$targetX-($enemyObject.X+$enemyObject.Width/2);$dy=$targetY-($enemyObject.Y+$enemyObject.Height/2)
$enemyObject.AttackCooldown--;if($enemyObject.ContactCooldown-gt0){$enemyObject.ContactCooldown--}
$oldEnemyY=$enemyObject.Y
switch($enemyObject.Kind){
'FlyerShooter' {
$enemyObject.VX=[Math]::Max(-$enemyObject.Speed,[Math]::Min($enemyObject.Speed,($enemyObject.VX*.92)+([Math]::Sign($dx)*.32)))
$enemyObject.VY=[Math]::Max(-3.2,[Math]::Min(3.2,($enemyObject.VY*.9)+([Math]::Sign($dy)*.18)))
if($enemyObject.AttackCooldown-le0){$enemyObject.AttackTicks=26;$enemyObject.AttackCooldown=[Math]::Max(70,160-[Math]::Floor($script:score/10))}
if($enemyObject.AttackTicks-gt0){
$enemyObject.AttackTicks--;Set-EnemyFrame $enemyObject $(if($enemyObject.AttackTicks-gt10){2}else{3})
if($enemyObject.AttackTicks-eq10){$length=[Math]::Max(1,[Math]::Sqrt($dx*$dx+$dy*$dy));New-Fireball -owner 'Enemy' -originX ($enemyObject.X+$enemyObject.Width/2) -originY ($enemyObject.Y+$enemyObject.Height/2) -velocityX (($dx/$length)*8.5) -velocityY (($dy/$length)*8.5)}
}else{Set-EnemyFrame $enemyObject ([Math]::Floor($script:elapsedTicks/12)%2)}
}
'Hopper' {
$enemyObject.VX=([Math]::Sign($dx)*$enemyObject.Speed)
$direction=[Math]::Sign($enemyObject.VX)
if($enemyObject.Grounded-and($enemyObject.AttackCooldown-le0-or(Test-EnemyObstacleAhead $enemyObject $direction))){$enemyObject.VY=-11.5;$enemyObject.Grounded=$false;$enemyObject.AttackCooldown=72}
}
'Roller' {
if($enemyObject.AttackCooldown-le0){$enemyObject.AttackTicks=58;$enemyObject.AttackCooldown=145}
$charging=$enemyObject.AttackTicks-gt0;if($charging){$enemyObject.AttackTicks--}
$enemyObject.VX=[Math]::Sign($dx)*$(if($charging){$enemyObject.Speed}else{2.0})
if($enemyObject.Grounded-and(Test-EnemyObstacleAhead $enemyObject ([Math]::Sign($enemyObject.VX)))){$enemyObject.VY=-8.5;$enemyObject.Grounded=$false}
}
'Swooper' {
if($enemyObject.AttackCooldown-le0){$enemyObject.AttackTicks=52;$enemyObject.AttackCooldown=125}
if($enemyObject.AttackTicks-gt0){
$enemyObject.AttackTicks--;$enemyObject.VX=[Math]::Max(-$enemyObject.Speed,[Math]::Min($enemyObject.Speed,($enemyObject.VX*.86)+([Math]::Sign($dx)*.72)))
$enemyObject.VY=[Math]::Max(-$enemyObject.Speed,[Math]::Min($enemyObject.Speed,($enemyObject.VY*.84)+([Math]::Sign($dy)*.62)));Set-EnemyFrame $enemyObject $(if($enemyObject.AttackTicks-gt30){2}else{3})
}else{
$hoverY=$enemyObject.HomeY+([Math]::Sin(($script:elapsedTicks/22)+$enemyObject.Phase)*45);$enemyObject.VX=[Math]::Max(-3.0,[Math]::Min(3.0,($enemyObject.VX*.94)+([Math]::Sign($dx)*.16)));$enemyObject.VY=($hoverY-$enemyObject.Y)*.08;Set-EnemyFrame $enemyObject ([Math]::Floor($script:elapsedTicks/14)%2)
}
}
'Boss' {
$enemyObject.VX=[Math]::Sign($dx)*$enemyObject.Speed
if($enemyObject.Grounded-and(Test-EnemyObstacleAhead $enemyObject ([Math]::Sign($enemyObject.VX)))){$enemyObject.VY=-10.5;$enemyObject.Grounded=$false}
if($enemyObject.AttackCooldown-le0){$enemyObject.AttackTicks=48;$enemyObject.AttackCooldown=105}
if($enemyObject.AttackTicks-gt0){
$enemyObject.AttackTicks--;Set-EnemyFrame $enemyObject $(if($enemyObject.AttackTicks-gt30){1}elseif($enemyObject.AttackTicks-gt14){2}else{3})
if($enemyObject.AttackTicks-eq14){foreach($angle in @(-.28,0,.28)){$length=[Math]::Max(1,[Math]::Sqrt($dx*$dx+$dy*$dy));$baseX=$dx/$length;$baseY=$dy/$length;New-Fireball -owner 'Enemy' -originX ($enemyObject.X+$enemyObject.Width/2) -originY ($enemyObject.Y+$enemyObject.Height*.45) -velocityX (($baseX*[Math]::Cos($angle)-$baseY*[Math]::Sin($angle))*7.2) -velocityY (($baseX*[Math]::Sin($angle)+$baseY*[Math]::Cos($angle))*7.2)}}
}
}
}
if($enemyObject.Kind-in@('Hopper','Roller','Boss')){$enemyObject.VY=[Math]::Min(22,$enemyObject.VY+.82)}
$enemyObject.X+=($enemyObject.VX*$script:enemyMovementScale)
$enemyObject.Y+=($enemyObject.VY*$script:enemyMovementScale)
$left=[Windows.SystemParameters]::VirtualScreenLeft;$right=$left+[Windows.SystemParameters]::VirtualScreenWidth-$enemyObject.Width
$enemyObject.X=[Math]::Max($left,[Math]::Min($right,$enemyObject.X))
if($enemyObject.Kind-in@('Hopper','Roller','Boss')){
Resolve-EnemyGround $enemyObject $oldEnemyY
if([Math]::Abs($enemyObject.VX)-gt.15){$enemyObject.VisualScale.ScaleX=$(if($enemyObject.VX-lt0){-1}else{1})}
if($enemyObject.Kind-eq'Hopper'){
Set-EnemyFrame $enemyObject $(if(-not$enemyObject.Grounded){$(if($enemyObject.VY-lt0){1}else{2})}else{([Math]::Floor($script:elapsedTicks/9)%2)*3})
}elseif($enemyObject.Kind-eq'Roller'){
Set-EnemyFrame $enemyObject $(if($enemyObject.AttackTicks-gt0){3}elseif(-not$enemyObject.Grounded){2}else{[Math]::Floor($script:elapsedTicks/9)%2})
}elseif($enemyObject.AttackTicks-le0){Set-EnemyFrame $enemyObject $(if($enemyObject.Grounded){[Math]::Floor($script:elapsedTicks/13)%2}else{1})}
}
if(($script:inputTick%2)-eq0){Set-SharedOverlayElementPosition $enemyObject.Element $enemyObject.X $enemyObject.Y}
if($enemyObject.ContactCooldown-le0){
$hitP1=$enemyObject.X+$enemyObject.Width-gt$script:x+8-and$enemyObject.X-lt$script:x+$PetWidth-8-and$enemyObject.Y+$enemyObject.Height-gt$script:y+8-and$enemyObject.Y-lt$script:y+$PetHeight-5
$hitP2=$script:twoPlayerActive-and$enemyObject.X+$enemyObject.Width-gt$script:p2X+8-and$enemyObject.X-lt$script:p2X+$PetWidth-8-and$enemyObject.Y+$enemyObject.Height-gt$script:p2Y+8-and$enemyObject.Y-lt$script:p2Y+$PetHeight-5
if($hitP1-and$script:invulnerableTicks-le0){$script:hearts--;$script:invulnerableTicks=90;$enemyObject.ContactCooldown=55}
elseif($hitP2-and$script:p2InvulnerableTicks-le0){$script:p2Hearts--;$script:p2InvulnerableTicks=90;$enemyObject.ContactCooldown=55}
}
}
function Remove-Enemy($enemyObject, [bool]$defeated) {
if ($null -ne $enemyObject) { Remove-SharedGameplayElement $enemyObject.Element;$script:enemies.Remove($enemyObject) }
if ($defeated) {
$script:score += $enemyObject.ScoreValue; $script:enemiesDefeated++
}
}
function Remove-AllEnemies {
foreach ($enemyObject in @($script:enemies)) { Remove-SharedGameplayElement $enemyObject.Element }
$script:enemies.Clear()
}
function New-Star {
$starWindow = New-Object Windows.Window
$starWindow.Title = 'Windowisp Star'
$starWindow.Width = 84; $starWindow.Height = 84
$starWindow.WindowStyle = [Windows.WindowStyle]::None; $starWindow.AllowsTransparency = $true
$starWindow.Background = [Windows.Media.Brushes]::Transparent; $starWindow.Topmost = $true
$starWindow.ShowInTaskbar = $false; $starWindow.ShowActivated = $false; $starWindow.ResizeMode = [Windows.ResizeMode]::NoResize
$canvas = New-Object Windows.Controls.Canvas
$canvas.Width=84;$canvas.Height=84;$canvas.IsHitTestVisible=$false
$polygon = New-Object Windows.Controls.Image
$polygon.Source=Get-ToyboxImageSource 'StarPickup';$polygon.Width=84;$polygon.Height=84;$polygon.Stretch='Uniform'
$polygon.RenderTransformOrigin = [Windows.Point]::new(0.5, 0.5)
$starTransforms = New-Object Windows.Media.TransformGroup
$starScale = [Windows.Media.ScaleTransform]::new(1,1)
$starRotate = [Windows.Media.RotateTransform]::new(0)
$starTransforms.Children.Add($starScale)|Out-Null;$starTransforms.Children.Add($starRotate)|Out-Null
$polygon.RenderTransform=$starTransforms
$canvas.Children.Add($polygon) | Out-Null
$left = [int][Windows.SystemParameters]::VirtualScreenLeft
$top = [int][Windows.SystemParameters]::VirtualScreenTop
$maxX = [int]($left + [Windows.SystemParameters]::VirtualScreenWidth - 120)
$minY = $top + 90
$maxY = [int]($top + [Windows.SystemParameters]::VirtualScreenHeight - 190)
$starX = $left + 20; $starY = $minY
foreach ($attempt in 1..12) {
$starX = $script:random.Next($left + 20, [Math]::Max($left + 21, $maxX))
$starY = $script:random.Next($minY, [Math]::Max($minY + 1, $maxY))
if ([Math]::Abs($starX - $script:x) + [Math]::Abs($starY - $script:y) -gt 260) { break }
}
Add-SharedGameplayElement $canvas $starX $starY
$script:star = [pscustomobject]@{ Window=$null;Element=$canvas; Shape=$polygon; Rotate=$starRotate; X=[double]$starX; Y=[double]$starY; Angle=0.0 }
}
function Remove-Star([bool]$collected) {
if ($null -ne $script:star) { Remove-SharedGameplayElement $script:star.Element;$script:star = $null }
if ($collected) {
$script:score += 10
$script:starsCollected++
if (($script:starsCollected % 3) -eq 0 -and $null -eq $script:shieldPickup) {
$script:shieldPickupSpawnTicks = 30
}
$script:starSpawnTicks = $script:random.Next(180, 361)
}
}
function Reset-Game {
foreach ($shot in @($script:projectiles)) { Remove-SharedGameplayElement $shot.Element }
$script:projectiles.Clear(); Remove-AllEnemies; Remove-Star $false; Remove-Shield; Remove-ShieldPickup; Clear-Dodgeballs
$script:score = 0; $script:hearts = 3; $script:elapsedTicks = 0; $script:lastBossWave = 0
$script:invulnerableTicks = 0; $script:enemySpawnTicks = 210; $script:starSpawnTicks = 150
$script:enemyNextSpawnAt = [DateTime]::UtcNow.AddSeconds(5)
$script:dodgeballNextSpawnAt = [DateTime]::UtcNow.AddSeconds(5); $script:dodgeballFromRight = $false
$script:p2Hearts=3;$script:p2InvulnerableTicks=0
$script:enemiesDefeated = 0; $script:starsCollected = 0; $script:shieldTicks = 0; $script:shieldPickupSpawnTicks = 45
$script:airJumpsRemaining = 1; $script:dashTicks = 0; $script:dashCooldown = 0
$script:charging = $false; $script:chargeTicks = 0; $script:attackCooldown = 0
$script:groundPlatformHwnd = [IntPtr]::Zero; $script:groundPlatformRect = $null
$script:p2GroundPlatformHwnd = [IntPtr]::Zero; $script:p2GroundPlatformRect = $null
if ($script:playroomActive) {
$script:enemySpawnTicks = 999999; $script:starSpawnTicks = 999999; $script:shieldPickupSpawnTicks = 999999
}
}
function Set-GameMode([bool]$enabled) {
if(-not$enabled-and($script:wispfallActive-or$null-ne$script:wispfallLavaWindow)){Remove-WispfallSetup}
if ($script:active -eq $enabled) { return }
if ($enabled -and $script:hearts -le 0) { Reset-Game }
$script:active = $enabled
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output "diagnostic:game-mode active=$($script:active)" }
Register-ControlKeys ($script:active -and -not $script:frozen)
Set-ClickThrough ($script:frozen -or -not $script:active)
Update-PlatformHighlights
$script:lastRenderKey = ''
Update-Menu
}
$window.Add_SourceInitialized({
try {
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output 'diagnostic:source-initialized-begin' }
$helper = New-Object Windows.Interop.WindowInteropHelper($window)
$script:hwnd = $helper.Handle
$script:source = [Windows.Interop.HwndSource]::FromHwnd($script:hwnd)
$script:source.AddHook({
param($hookHwnd, $msg, $wParam, $lParam, [ref]$handled)
if ($msg -ne [PetNative]::WM_HOTKEY) { return [IntPtr]::Zero }
$id = $wParam.ToInt32(); $handled.Value = $true
if ($id -eq $hotkeyIds.Toggle) {
Set-GameMode (-not $script:active)
} elseif ($id -eq $hotkeyIds.Reset) {
Restart-CurrentGame
} elseif ($id -eq $hotkeyIds.Close) {
$window.Close()
} elseif ($id -eq $hotkeyIds.Menu) {
Toggle-Menu
} elseif ($id -eq $hotkeyIds.Freeze) {
Set-Frozen (-not $script:frozen)
} elseif ($id -eq $hotkeyIds.Exit) {
Invoke-ExitControl
}
return [IntPtr]::Zero
})
[PetNative]::RegisterHotKey($script:hwnd, $hotkeyIds.Toggle, [PetNative]::MOD_NOREPEAT, $vk.F8) | Out-Null
[PetNative]::RegisterHotKey($script:hwnd, $hotkeyIds.Reset, [PetNative]::MOD_NOREPEAT, $vk.F9) | Out-Null
[PetNative]::RegisterHotKey($script:hwnd, $hotkeyIds.Close, [PetNative]::MOD_NOREPEAT, $vk.F10) | Out-Null
$menuModifiers = [PetNative]::MOD_CONTROL -bor [PetNative]::MOD_ALT -bor [PetNative]::MOD_NOREPEAT
[PetNative]::RegisterHotKey($script:hwnd, $hotkeyIds.Menu, $menuModifiers, $vk.P) | Out-Null
[PetNative]::RegisterHotKey($script:hwnd, $hotkeyIds.Freeze, [PetNative]::MOD_NOREPEAT, $vk.F7) | Out-Null
Set-ClickThrough $true
New-HudWindows
New-ControlCentre
$script:platforms = [PetNative]::GetPlatformWindows([uint32]$PID)
Update-PlatformHighlights
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_WINDOW_RIDE-eq'1'){
$ridePlatform=@($script:platforms|Select-Object -First 1)[0]
if($null-ne$ridePlatform){
$rideBeforeX=$script:x;$rideBeforeY=$script:y
$script:grounded=$true;$script:groundPlatformHwnd=$ridePlatform.Hwnd
$script:groundPlatformRect=[pscustomobject]@{Left=([double]$ridePlatform.Left-37);Top=([double]$ridePlatform.Top-19);Right=$ridePlatform.Right;Bottom=$ridePlatform.Bottom}
Update-RidingApplicationWindow 'P1'
$testLeftDown=$true;$testRightDown=$true
$overlapHorizontal=$(if($testLeftDown-and-not$testRightDown){-1}elseif($testRightDown-and-not$testLeftDown){1}else{0})
$validRide=([Math]::Abs(($script:x-$rideBeforeX)-37)-lt.01)-and([Math]::Abs(($script:y-$rideBeforeY)-19)-lt.01)
$script:grounded=$false;Update-RidingApplicationWindow 'P1'
$jumpDetached=([Math]::Abs($script:x-($rideBeforeX+37))-lt.01)-and([Math]::Abs($script:y-($rideBeforeY+19))-lt.01)
$script:grounded=$true;$script:groundPlatformHwnd=[IntPtr]::new(123456789);$script:groundPlatformRect=$ridePlatform
Update-RidingApplicationWindow 'P1'
$invalidDetached=$script:groundPlatformHwnd-eq[IntPtr]::Zero-and-not$script:grounded
[Console]::Out.WriteLine("diagnostic:window-ride validMove=$validRide overlapAD=$overlapHorizontal jumpDetached=$jumpDetached invalidDetached=$invalidDetached")
}else{[Console]::Out.WriteLine('diagnostic:window-ride skipped=no-platform-window')}
}
if ($env:WINDOWISP_TEST_RESTART_SURVIVAL -eq '1') {
Start-Playroom
Restart-CurrentGame
} elseif ($env:WINDOWISP_TEST_PLAYROOM -eq '1') {
Start-Playroom
}
if ($env:WINDOWISP_TEST_CONTROLS -eq '1') { $script:menuWindow.Show(); Show-ControlOverlay }
if ($env:WINDOWISP_TEST_DODGEBALL -eq '1') {
Start-GameType 'Dodgeball'
if ($env:WINDOWISP_TEST_DODGEBALL_FAST -eq '1') { $script:dodgeballNextSpawnAt = [DateTime]::UtcNow.AddMilliseconds(50) }
}
if ($env:WINDOWISP_TEST_TWO_PLAYER -eq '1') { Set-TwoPlayerMode $true; Set-GameMode $true }
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_P2_FOOTBALL_CONTROLLER-eq'1'){
Set-TwoPlayerMode $false;Set-Player2Controller 'Human';Start-Football
$singlePlayerController=$script:p2ControllerMode
Return-ToSandbox;Set-TwoPlayerMode $true;Set-Player2Controller 'Human';Start-Football
[Console]::Out.WriteLine("diagnostic:p2-football-controller single=$singlePlayerController existingTwoPlayer=$($script:p2ControllerMode)")
}
if ($env:WINDOWISP_TEST_ACTIVE -eq '1') { Set-GameMode $true }
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_ENEMY_ROSTER-eq'1'){
Start-GameType 'Survival';Remove-AllEnemies
foreach($enemyType in @('EmberImp','CinderMoth','MossHopper','ShellbackRoller','StormJelly','GloamGolem')){New-Enemy $enemyType}
[Console]::Out.WriteLine("diagnostic:enemy-roster count=$($script:enemies.Count) types=$(@($script:enemies|ForEach-Object{$_.Type})-join',') health=$(@($script:enemies|ForEach-Object{$_.Health})-join',')")
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_TUTORIAL -eq '1') {
Show-QuickTutorial $true
[Console]::Out.WriteLine("diagnostic:tutorial visible=$($script:tutorialWindow.IsVisible) width=$($script:tutorialWindow.Width) height=$($script:tutorialWindow.Height)")
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_PET_SCALE -eq '1') {
[Console]::Out.WriteLine("diagnostic:pet-scale initial=$($script:petScalePercent) width=$PetWidth height=$PetHeight")
Set-PetScale 70 $false
[Console]::Out.WriteLine("diagnostic:pet-scale small=$($script:petScalePercent) width=$PetWidth height=$PetHeight window=$($window.Width)x$($window.Height)")
Set-PetScale 85 $false
[Console]::Out.WriteLine("diagnostic:pet-scale compact=$($script:petScalePercent) width=$PetWidth height=$PetHeight window=$($window.Width)x$($window.Height)")
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_UI_REVIEW -eq '1') {
if(-not$script:playroomActive){Start-Playroom}
if($null-eq$script:sandboxHotbarWindow){New-SandboxToolbox}
Set-SandboxToolboxVisible $true;Show-SandboxCategory 'Blocks'
if(-not$script:menuWindow.IsVisible){$script:menuWindow.Show()}
$script:creditsPanel.Visibility=[Windows.Visibility]::Visible
[Console]::Out.WriteLine("diagnostic:ui-review menu=$($script:menuWindow.IsVisible) hotbar=$($script:sandboxHotbarWindow.IsVisible) tray=$($script:sandboxTrayWindow.IsVisible) hotbarWidth=$($script:sandboxHotbarWindow.Width) wip=$($script:wipPowerupButton.Tag) credits=$($script:creditsPanel.Visibility) creditsChildren=$($script:creditsPanel.Children.Count)")
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_CONTEXT_MENUS -eq '1') {
if(-not$script:playroomActive){Start-Playroom}
$testRotator=[pscustomobject]@{Kind='Rotator';ManualPower=$null;Direction=1;RotationSpeed=1.0;Target=$null}
$testFire=[pscustomobject]@{Kind='FireJet';ManualPower=$null}
$testRope=[pscustomobject]@{Kind='Rope'}
$rotatorMenu=New-PlacementContextMenu $testRotator $false
$fireMenu=New-PlacementContextMenu $testFire $true
$ropeMenu=New-PlacementContextMenu $testRope $false
$blockMenu=@($script:playroomBlocks|Select-Object -First 1)[0].ContextMenu
$headers={param($menu) (@($menu.Items|ForEach-Object{[string]$_.Header}) -join '|')}
$blockChecks=@($blockMenu.Items|Where-Object{$_.Header-eq'Block Colour'}|ForEach-Object{@($_.Items|Where-Object{$_.IsChecked})}).Count
$firePower=@($fireMenu.Items|Where-Object{$_.Header-eq'Power'}|ForEach-Object{@($_.Items|ForEach-Object{[string]$_.Header})}) -join '|'
$menuColour=$rotatorMenu.Background.Color
[Console]::Out.WriteLine("diagnostic:context-menus block=$(& $headers $blockMenu) rotator=$(& $headers $rotatorMenu) fire=$(& $headers $fireMenu) rope=$(& $headers $ropeMenu) blockColourChecks=$blockChecks firePower=$firePower background=$($menuColour.R),$($menuColour.G),$($menuColour.B)")
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and $env:WINDOWISP_TEST_PET_SELECTOR -eq '1') {
Show-PetSelector
$selectorPetCount=@($script:petSelectorPanel.Children|Where-Object{$_-is[Windows.Controls.Button]-and$null-ne$_.Tag-and$null-ne$_.Tag.PSObject.Properties['Id']}).Count
[Console]::Out.WriteLine("diagnostic:pet-selector visible=$($script:petSelectorWindow.IsVisible) pets=$selectorPetCount sizeButtons=$($script:petSizeButtons.Count) selected=$($script:petScalePercent)")
}
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output 'diagnostic:source-initialized-end' }
} catch {
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Error "diagnostic:source-initialized-error $($_.Exception.ToString())" }
throw
}
})
$script:modeTransitionStressTick = 0
$timer = New-Object Windows.Threading.DispatcherTimer
# Request below the common 15.625 ms Windows timer quantum. A 16-17 ms
# DispatcherTimer can round up to ~31 ms and visibly cap window motion near 30 FPS.
$timer.Interval = [TimeSpan]::FromMilliseconds(10)
$timer.Add_Tick({
if($script:eraserExitGuardTicks-gt0){$script:eraserExitGuardTicks--}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_MODE_TRANSITIONS-eq'1'){
$script:modeTransitionStressTick++
if($script:modeTransitionStressTick-ge30-and(($script:modeTransitionStressTick-30)%60)-eq0){
$phase=[int](($script:modeTransitionStressTick-30)/60)%10
$cycle=[int][Math]::Floor(($script:modeTransitionStressTick-30)/600)+1
$transitionName=''
try{
switch($phase){
0 {$transitionName='Sandbox';Set-TwoPlayerMode $true;Return-ToSandbox}
1 {$transitionName='Survival';Start-GameType 'Survival'}
2 {$transitionName='Dodgeball';Start-GameType 'Dodgeball'}
3 {$transitionName='Sandbox return';Start-Playroom}
4 {$transitionName='Football';Start-Football}
5 {$transitionName='Football to Sandbox';Return-ToSandbox}
6 {$transitionName='Wispfall';Start-Wispfall}
7 {$transitionName='Wispfall to Sandbox';Return-ToSandbox}
8 {$transitionName='Survival return';Start-GameType 'Survival'}
9 {$transitionName='P2 rebuild';Set-TwoPlayerMode $false;Set-TwoPlayerMode $true}
}
$mode=$(if($script:footballActive){'Football'}elseif($script:wispfallActive){'Wispfall'}elseif($script:playroomActive){'Sandbox'}else{$script:gameType})
[Console]::Out.WriteLine("diagnostic:transition cycle=$cycle phase=$phase requested=$transitionName mode=$mode active=$($script:active) p2=$($script:twoPlayerActive) blocks=$($script:playroomBlocks.Count) gadgets=$($script:playroomGadgets.Count) balls=$($script:playroomBalls.Count) dodgeballs=$($script:dodgeballs.Count) footballObjects=$(@($script:footballModeObjects).Count) lava=$($null-ne$script:wispfallLavaWindow)")
}catch{
[Console]::Error.WriteLine("diagnostic:transition-error cycle=$cycle phase=$phase requested=$transitionName $($_.Exception.ToString())")
}
}
}
if($env:WINDOWISP_TEST_AUTO_EXIT_TICKS){
$script:diagnosticTickCount++
if($script:diagnosticTickCount-ge[int]$env:WINDOWISP_TEST_AUTO_EXIT_TICKS){$window.Close();return}
}
if ($null -ne $script:frameClock) {
$nowMs = $script:frameClock.Elapsed.TotalMilliseconds
if ($script:lastFrameMs -gt 0) {
$gap = $nowMs - $script:lastFrameMs
$script:frameGapMax = [Math]::Max($script:frameGapMax, $gap)
$script:frameGapSum += $gap; $script:frameGapSamples++
}
$script:lastFrameMs = $nowMs
}
if ($gameExitEvent.WaitOne(0)) { $window.Close(); return }
if ($gameOnEvent.WaitOne(0)) {
if (-not $script:startupModeChosen) {
$script:startupModeChosen = $true
Start-Playroom
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { [Console]::Out.WriteLine("diagnostic:startup-mode Sandbox blocks=$($script:playroomBlocks.Count) balls=$($script:playroomBalls.Count)") }
if ($env:WINDOWISP_DIAGNOSTICS -ne '1' -and -not (Test-Path -LiteralPath $script:tutorialSeenPath -PathType Leaf)) { Show-QuickTutorial $true }
} elseif ($script:playroomActive) {
Start-GameType $script:gameType
} else {
Set-GameMode $true
}
}
if ($gameOffEvent.WaitOne(0)) { Set-GameMode $false }
if ($gameToggleEvent.WaitOne(0)) { Set-GameMode (-not $script:active) }
if ($playroomEvent.WaitOne(0)) {
$script:startupModeChosen = $true
if ($script:playroomActive) { Stop-Playroom } else { Start-Playroom }
}
if($petSwitchEvent.WaitOne(0)){
$switchPath=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Windowisp\pet-switch.json'
if(Test-Path -LiteralPath $switchPath -PathType Leaf){
try{
$request=Get-Content -LiteralPath $switchPath -Raw|ConvertFrom-Json
Set-ActivePetAtlas ([string]$request.atlasPath) ([string]$request.id) ([string]$request.displayName) ([string]$request.player)
}catch{}
}
}
if($familiarEventSignal.WaitOne(0)){Receive-FamiliarEvent}
if(-not$script:p2AITestInitialized-and$env:WINDOWISP_TEST_P2_AI-in@('Opponent','Friend')-and$script:playroomActive){
$script:p2AITestInitialized=$true
Set-Player2Controller $env:WINDOWISP_TEST_P2_AI
$script:active=$true
[Console]::Out.WriteLine("diagnostic:p2-ai-start mode=$($script:p2ControllerMode) balls=$($script:playroomBalls.Count)")
}
if(-not$script:enemyRosterTestInitialized-and$env:WINDOWISP_TEST_ENEMY_ROSTER-eq'1'){
$script:enemyRosterTestInitialized=$true
Start-GameType 'Survival';Remove-AllEnemies
foreach($enemyType in @('EmberImp','CinderMoth','MossHopper','ShellbackRoller','StormJelly','GloamGolem')){New-Enemy $enemyType}
$script:invulnerableTicks=999999;$script:p2InvulnerableTicks=999999
[Console]::Out.WriteLine("diagnostic:enemy-roster-active count=$($script:enemies.Count) mode=$($script:gameType) playroom=$($script:playroomActive)")
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_ENEMY_ROSTER-eq'1'-and($script:inputTick%120)-eq0-and$script:enemies.Count-gt0){
$states=@($script:enemies|ForEach-Object{"$($_.Type):$([Math]::Round($_.X)),$([Math]::Round($_.Y)),f$($_.Frame),hp$($_.Health),g$($_.Grounded),sx$($_.VisualScale.ScaleX)"})-join'|'
[Console]::Out.WriteLine("diagnostic:enemy-roster-state projectiles=$($script:projectiles.Count) $states")
}
if(-not$script:footballTestInitialized-and$env:WINDOWISP_TEST_FOOTBALL-eq'1'-and$script:playroomActive){
$script:footballTestInitialized=$true
Start-Football
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine('diagnostic:football-started')}
if($env:WINDOWISP_TEST_MINIGAME_EXIT-eq'1'){
Return-ToSandbox
[Console]::Out.WriteLine("diagnostic:football-exit active=$($script:active) sandbox=$($script:playroomActive) football=$($script:footballActive) goals=$(@($script:playroomGadgets|Where-Object{$_.FootballGoal}).Count) hotbar=$($script:sandboxHotbarWindow.IsVisible)")
}elseif($env:WINDOWISP_TEST_FOOTBALL_RESTART-eq'1'){
$script:footballP1Score=2;$script:footballP2Score=1
Restart-CurrentGame
[Console]::Out.WriteLine("diagnostic:football-restart p1=$($script:footballP1Score) p2=$($script:footballP2Score) countdown=$($script:footballCountdownTicks) balls=$(@($script:playroomBalls|Where-Object{$_.FootballBall}).Count) goals=$(@($script:playroomGadgets|Where-Object{$_.FootballGoal}).Count)")
}
}
if(-not$script:wispfallTestInitialized-and$env:WINDOWISP_TEST_WISPFALL-eq'1'-and$script:playroomActive){
$script:wispfallTestInitialized=$true
Set-PetScale 115 $false
Add-PlayroomHazard 'FireJet'
$beforeBlocks=$script:playroomBlocks.Count;$beforeGadgets=$script:playroomGadgets.Count
Start-Wispfall
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){
[Console]::Out.WriteLine("diagnostic:wispfall-started scale=$($script:petScalePercent) generatedBlocks=$(@($script:playroomBlocks|Where-Object{$_.WispfallGenerated}).Count) userBlocksActive=$(@($script:playroomBlocks|Where-Object{-not$_.WispfallGenerated}).Count) userGadgetsActive=$(@($script:playroomGadgets|Where-Object{$null-eq$_.PSObject.Properties['WispfallGenerated']}).Count) storedBlocks=$($script:wispfallStoredBlocks.Count) storedGadgets=$($script:wispfallStoredGadgets.Count)")
}
if($env:WINDOWISP_TEST_MINIGAME_EXIT-eq'1'){
Return-ToSandbox
[Console]::Out.WriteLine("diagnostic:wispfall-exit scale=$($script:petScalePercent) blocks=$($script:playroomBlocks.Count)/$beforeBlocks gadgets=$($script:playroomGadgets.Count)/$beforeGadgets stored=$($script:wispfallStoredBlocks.Count),$($script:wispfallStoredGadgets.Count) lava=$($null-ne$script:wispfallLavaWindow)")
}
}
if ($script:frozen) { return }
$p1Ladder=$null
if ($script:active) {
$script:inputTick++
if($script:gadgetBenchmarkPhase-like'Ladders*Moving'){
$benchMotionX=[Math]::Sin($script:inputTick*.11)*75;$benchMotionY=[Math]::Cos($script:inputTick*.07)*28
foreach($benchLadder in @($script:playroomBlocks|Where-Object{$_.Type-eq'Ladder'-and$null-ne$_.PSObject.Properties['BenchmarkBaseX']})){
$benchLadder.X=$benchLadder.BenchmarkBaseX+$benchMotionX;$benchLadder.Y=$benchLadder.BenchmarkBaseY+$benchMotionY
Set-PlacementWindowPosition $benchLadder
}
}
$leftDown = (Test-KeyDown $script:controlBindings.P1Left) -or (-not $script:twoPlayerActive -and (Test-KeyDown $vk.Left))
$rightDown = (Test-KeyDown $script:controlBindings.P1Right) -or (-not $script:twoPlayerActive -and (Test-KeyDown $vk.Right))
$jumpDown = (Test-KeyDown $script:controlBindings.P1Jump) -or (Test-KeyDown $vk.W) -or (-not $script:twoPlayerActive -and (Test-KeyDown $vk.Up))
$dropDown = (Test-KeyDown $script:controlBindings.P1Drop) -or (-not $script:twoPlayerActive -and (Test-KeyDown $vk.Down))
$attackDown = Test-KeyDown $script:controlBindings.P1Attack
$interactDown = Test-KeyDown $script:controlBindings.P1Interact
$p1Ladder=Get-LadderContact $script:x $script:y
$p1Climbing=$null-ne$p1Ladder
$p1ClimbUpDown=(Test-KeyDown $vk.W)-or(-not$script:twoPlayerActive-and(Test-KeyDown $vk.Up))
if ($leftDown -and -not $script:previousKeys.Left) {
if (-not $script:grounded -and $script:dashCooldown -le 0 -and ($script:inputTick - $script:lastLeftTapTick) -le 16) {
$script:vx = -18*$script:baseMovementScale; $script:vy *= 0.2; $script:dashTicks = 8; $script:dashCooldown = 90
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output 'diagnostic:air-dash-left' }
}
$script:lastLeftTapTick = $script:inputTick
}
if ($rightDown -and -not $script:previousKeys.Right) {
if (-not $script:grounded -and $script:dashCooldown -le 0 -and ($script:inputTick - $script:lastRightTapTick) -le 16) {
$script:vx = 18*$script:baseMovementScale; $script:vy *= 0.2; $script:dashTicks = 8; $script:dashCooldown = 90
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output 'diagnostic:air-dash-right' }
}
$script:lastRightTapTick = $script:inputTick
}
$horizontal = $(if ($leftDown -and -not $rightDown) { -1 } elseif ($rightDown -and -not $leftDown) { 1 } else { 0 })
if ($horizontal -ne 0 -and $script:dashTicks -le 0) {
$speedScale = $script:petSpeedScales[$script:petSpeedIndex]*$script:baseMovementScale
$acceleration = $(if ($script:grounded) { 1.35 } else { 0.72 }) * $speedScale
$maxSpeed = 9.5 * $speedScale
$script:vx = [Math]::Max(-$maxSpeed, [Math]::Min($maxSpeed, $script:vx + ($horizontal * $acceleration)))
$script:facing = $horizontal
} elseif ($script:grounded) {
$script:vx *= $(if ($script:surfaceEffect -eq 'Ice' -and $script:surfaceEffectTicks -gt 0) { 0.975 } else { 0.78 })
} else {
$script:vx *= 0.985
}
$p1LadderJump=$p1Climbing-and$jumpDown-and-not$script:previousKeys.Jump-and$horizontal-ne0
if($p1LadderJump){
$script:vy=-15.5*$script:baseMovementScale;$script:vx=$horizontal*11.5*$script:baseMovementScale
$script:grounded=$false;$p1Climbing=$false;$p1Ladder=$null
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){Write-Output 'diagnostic:ladder-jump'}
}elseif ($jumpDown -and -not $script:previousKeys.Jump -and -not$p1Climbing) {
if ($script:grounded -or $script:coyoteTicks -gt 0) {
$script:vy = -18.5*$script:baseMovementScale; $script:grounded = $false; $script:coyoteTicks = 0
$script:airJumpsRemaining = 1
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output 'diagnostic:ground-jump' }
} elseif ($script:wallGraceTicks -gt 0 -and $script:wallContact -ne 0) {
$script:vy = -17.5*$script:baseMovementScale; $script:vx = -$script:wallContact * 13.5*$script:baseMovementScale
$script:facing = -$script:wallContact; $script:wallGraceTicks = 0
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output "diagnostic:wall-jump side=$($script:wallContact)" }
} elseif ($script:airJumpsRemaining -gt 0) {
$script:vy = -17*$script:baseMovementScale; $script:airJumpsRemaining--
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output 'diagnostic:double-jump' }
}
}
if($p1Climbing){
$climbDirection=$(if($p1ClimbUpDown){1}elseif($dropDown){-1}else{0})
$axisVelocity=($script:vx*$p1Ladder.UpX)+($script:vy*$p1Ladder.UpY)
$targetAxisVelocity=$climbDirection*5.5*$script:baseMovementScale
$script:vx+=($targetAxisVelocity-$axisVelocity)*$p1Ladder.UpX
$script:vy+=($targetAxisVelocity-$axisVelocity)*$p1Ladder.UpY
$script:grounded=$false
}
if ($dropDown -and -not $script:previousKeys.Drop) {
$script:dropTicks = 18; $script:grounded = $false; $script:vy = [Math]::Max(4, $script:vy)
$script:groundPlatformHwnd=[IntPtr]::Zero;$script:groundPlatformRect=$null
}
if ($attackDown -and -not $script:previousKeys.Attack -and $script:attackCooldown -le 0) {
if(Start-BatCharge 'Player1'){$script:charging=$false}
else{
$script:charging = $true; $script:chargeTicks = 0
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output 'diagnostic:charge-start' }
}
}
if ($interactDown -and -not $script:previousKeys.Interact) { Toggle-BallCarry 'Player1' }
if ($script:charging -and $attackDown) {
$script:chargeTicks = [Math]::Min(90, $script:chargeTicks + 1)
}
if($attackDown){Update-BatCharge 'Player1'}
if(-not$attackDown-and$script:previousKeys.Attack-and(Release-BatSwing 'Player1')){
$script:charging=$false;$script:attackTicks=18;$script:attackCooldown=22
}
if ($script:charging -and -not $attackDown -and $script:previousKeys.Attack) {
$chargePower = $script:chargeTicks / 90
New-Fireball -power $chargePower
$script:charging = $false; $script:attackTicks = 18; $script:attackCooldown = 24
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output "diagnostic:charge-release power=$chargePower" }
}
$script:previousKeys.Jump = $jumpDown; $script:previousKeys.Drop = $dropDown; $script:previousKeys.Attack = $attackDown; $script:previousKeys.Interact = $interactDown
$script:previousKeys.Left = $leftDown; $script:previousKeys.Right = $rightDown
} else {
$script:previousKeys.Jump = $false; $script:previousKeys.Drop = $false; $script:previousKeys.Attack = $false; $script:previousKeys.Interact = $false
$script:previousKeys.Left = $false; $script:previousKeys.Right = $false
if ($script:autoWander -and $script:grounded) {
if ($script:facing -eq 0) { $script:facing = 1 }
$script:vx = $script:facing * (1.4 * $script:petSpeedScales[$script:petSpeedIndex] * $script:baseMovementScale)
$wanderLeft = [Windows.SystemParameters]::VirtualScreenLeft + 12
$wanderRight = $wanderLeft + [Windows.SystemParameters]::VirtualScreenWidth - $PetWidth - 24
if ($script:x -le $wanderLeft) { $script:facing = 1 }
if ($script:x -ge $wanderRight) { $script:facing = -1 }
} elseif ($script:grounded) { $script:vx *= 0.78 }
}
if ($script:attackCooldown -gt 0) { $script:attackCooldown-- }
if ($script:attackTicks -gt 0) { $script:attackTicks-- }
if ($script:dashCooldown -gt 0) { $script:dashCooldown-- }
if ($script:dashTicks -gt 0) { $script:dashTicks-- }
# Hit protection belongs to the player, not to a particular game type.
# Sandbox hazards use the same flash and always recover to full opacity.
if ($script:invulnerableTicks -gt 0) {
$script:invulnerableTicks--
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$env:WINDOWISP_TEST_LASER_VISUAL-eq'1'-and$script:invulnerableTicks-eq0){
$script:x=[Windows.SystemParameters]::VirtualScreenLeft+40
[Console]::Out.WriteLine('diagnostic:sandbox-protection-recovered')
}
}
if ($script:p2InvulnerableTicks -gt 0) { $script:p2InvulnerableTicks-- }
if ($script:active -and $script:hearts -gt 0 -and -not $script:playroomActive) {
$script:elapsedTicks++
if ($script:gameType -eq 'Dodgeball') {
if ([DateTime]::UtcNow -ge $script:dodgeballNextSpawnAt) {
New-Dodgeball
$script:dodgeballNextSpawnAt = [DateTime]::UtcNow.AddSeconds(5)
}
Update-Dodgeballs
} else {
if ($env:WINDOWISP_DIAGNOSTICS -eq '1' -and ($script:elapsedTicks % 60) -eq 0) {
Write-Output "diagnostic:survival-tick elapsed=$($script:elapsedTicks) enemy-spawn=$($script:enemySpawnTicks) star-spawn=$($script:starSpawnTicks) shield-spawn=$($script:shieldPickupSpawnTicks)"
}
if ($null -eq $script:star) {
$script:starSpawnTicks--
if ($script:starSpawnTicks -le 0) { New-Star }
} else {
$script:star.Angle = ($script:star.Angle + 4.5) % 360
$script:star.Rotate.Angle = $script:star.Angle
}
if ($null -eq $script:shieldPickup -and $script:shieldPickupSpawnTicks -gt 0) {
$script:shieldPickupSpawnTicks--
if ($script:shieldPickupSpawnTicks -le 0) {
New-ShieldPickup
$script:shieldPickupSpawnTicks = -1
}
} elseif ($null -ne $script:shieldPickup) {
Update-ShieldPickup
}
$wave=Get-SurvivalWave
$enemyCap=[Math]::Min(9,3+[Math]::Ceiling($wave/2))
if ([DateTime]::UtcNow -ge $script:enemyNextSpawnAt) {
if ($script:enemies.Count -lt $enemyCap) { New-Enemy }
$script:enemyNextSpawnAt=[DateTime]::UtcNow.AddSeconds([Math]::Max(1.8,5-($wave*.35)))
}
foreach ($enemyObject in @($script:enemies)) {
Update-OneEnemy $enemyObject
}
}
}
$script:platformRefresh++
# Desktop discovery is comparatively expensive (native enumeration plus
# highlight reconciliation). A ~300 ms cadence still follows moved windows
# smoothly through the dedicated riding code without taxing crowded games.
if ($script:platformRefresh -ge 30) {
$script:platformRefresh = 0
$script:platforms = [PetNative]::GetPlatformWindows([uint32]$PID)
Update-PlatformHighlights
}
Update-RidingApplicationWindow 'P1'
$oldX = $script:x
$oldY = $script:y
$oldBottom = $script:y + $PetHeight
$gravity = $(if ($null-ne$p1Ladder) { 0 } elseif ($script:dashTicks -gt 0) { 0.15 } else { 1.05 })
$script:vy = [Math]::Min($script:vy + $gravity, 24)
$script:x += $script:vx
$script:y += $script:vy
if ([Math]::Abs($script:vx) -lt 0.15) { $script:vx = 0 }
if ($script:dropTicks -gt 0) { $script:dropTicks-- }
$leftBound = [Windows.SystemParameters]::VirtualScreenLeft
$topBound = [Windows.SystemParameters]::VirtualScreenTop
$rightBound = $leftBound + [Windows.SystemParameters]::VirtualScreenWidth - $PetWidth
$floor = $topBound + [Windows.SystemParameters]::VirtualScreenHeight
if ($script:wallGraceTicks -gt 0) { $script:wallGraceTicks-- } else { $script:wallContact = 0 }
if ($script:x -le $leftBound -and $script:vx -lt 0) {
$script:x = $leftBound; $script:vx = 0
$script:wallContact = -1; $script:wallGraceTicks = 6
if ($script:vy -gt 4.5) { $script:vy = 4.5 }
} elseif ($script:x -ge $rightBound -and $script:vx -gt 0) {
$script:x = $rightBound; $script:vx = 0
$script:wallContact = 1; $script:wallGraceTicks = 6
if ($script:vy -gt 4.5) { $script:vy = 4.5 }
}
$script:x = [Math]::Max($leftBound, [Math]::Min($rightBound, $script:x))
$script:y = [Math]::Max($topBound, $script:y)
$script:grounded = $false
foreach ($blockData in @($script:playroomBlocks)) {
if($blockData.Type-in@('Ladder','Wooden','Cloud')){continue}
if(Test-LadderBlockPassThrough $p1Ladder $blockData){continue}
if([Math]::Abs($blockData.Angle)-gt.01){continue}
$oldBodyTop=$oldY+8;$newBodyTop=$script:y+8
$newLeft=$script:x+12;$newRight=$script:x+$PetWidth-12
$horizontalOverlap=$newRight-gt$blockData.X -and $newLeft-lt$blockData.X+$blockData.Width
if($script:vy-lt0-and$horizontalOverlap-and$oldBodyTop-ge$blockData.Y+$blockData.Height-6-and$newBodyTop-le$blockData.Y+$blockData.Height){
$script:y=$blockData.Y+$blockData.Height-8;$script:vy=0;continue
}
$bodyTop = $script:y + 8; $bodyBottom = $script:y + $PetHeight - 5
if ($bodyBottom -le $blockData.Y -or $bodyTop -ge $blockData.Y + $blockData.Height) { continue }
$oldLeft = $oldX + 12; $oldRight = $oldX + $PetWidth - 12
if ($script:vx -gt 0 -and $oldRight -le $blockData.X + 5 -and $newRight -ge $blockData.X) {
$script:x = $blockData.X - ($PetWidth - 12); $script:vx = 0
$script:wallContact = 1; $script:wallGraceTicks = 6
if ($script:vy -gt 4.5) { $script:vy = 4.5 }
} elseif ($script:vx -lt 0 -and $oldLeft -ge $blockData.X + $blockData.Width - 5 -and $newLeft -le $blockData.X + $blockData.Width) {
$script:x = $blockData.X + $blockData.Width - 12; $script:vx = 0
$script:wallContact = -1; $script:wallGraceTicks = 6
if ($script:vy -gt 4.5) { $script:vy = 4.5 }
}
}
$newBottom = $script:y + $PetHeight
$landingY = $floor
$landingBlock = $null
$landingPlatform = $null
if ($script:vy -ge 0) {
for($platformIndex=0;$script:dropTicks-le0-and$platformIndex-lt$script:platforms.Count;$platformIndex++) {
$r=$script:platforms[$platformIndex]
$overlaps = ($script:x + $PetWidth - 12 -gt $r.Left) -and ($script:x + 12 -lt $r.Right)
if ($overlaps -and $oldBottom -le ($r.Top + 5) -and $newBottom -ge $r.Top -and $r.Top -lt $landingY -and (Test-PlatformTopExposed $platformIndex ($script:x+12) ($script:x+$PetWidth-12) $r.Top)) {
$landingY = $r.Top;$landingPlatform=$r
}
}
if ($script:playroomBlocks.Count -gt 0) {
foreach ($blockData in @($script:playroomBlocks)) {
if($blockData.Type-in@('Ladder','Wooden','Cloud')-or$blockData.TemporaryState-eq'Gone'){continue}
if(Test-LadderBlockPassThrough $p1Ladder $blockData){continue}
if([Math]::Abs($blockData.Angle)-gt.01){continue}
$blockLeft = $blockData.X
$blockTop = $blockData.Y
$overlaps = ($script:x + $PetWidth - 12 -gt $blockLeft) -and ($script:x + 12 -lt $blockLeft + $blockData.Width)
if ($overlaps -and $oldBottom -le ($blockTop + 6) -and $newBottom -ge $blockTop -and $blockTop -lt $landingY) {
$landingY = $blockTop; $landingBlock = $blockData;$landingPlatform=$null
}
}
}
}
if ($newBottom -ge $landingY) {
$script:y = $landingY - $PetHeight
$script:vy = 0; $script:grounded = $true
if($null-ne$landingPlatform){$script:groundPlatformHwnd=$landingPlatform.Hwnd;$script:groundPlatformRect=$landingPlatform}else{$script:groundPlatformHwnd=[IntPtr]::Zero;$script:groundPlatformRect=$null}
$script:surfaceEffect = $(if ($null -ne $landingBlock) { $landingBlock.Type } else { 'Normal' })
$script:surfaceEffectTicks = 8
if($null-ne$landingBlock){Trigger-TemporaryBlock $landingBlock 'Player1'}
if ($null -ne $landingBlock -and $landingBlock.Type -eq 'Bouncy') {
$script:vy = -20.5; $script:grounded = $false
} elseif ($null -ne $landingBlock -and $landingBlock.Type -eq 'Fire') {
$script:vy = -13; $script:vx += $(if ($script:facing -lt 0) { -3 } else { 3 })
$script:grounded = $false; $script:attackTicks = 20
} elseif ($null -ne $landingBlock -and $landingBlock.Type -eq 'Conveyor') {
$script:vx = [Math]::Max(-13, [Math]::Min(13, $script:vx + ($landingBlock.Direction * .72)))
}
}else{$script:groundPlatformHwnd=[IntPtr]::Zero;$script:groundPlatformRect=$null}
foreach($rotatedBlock in @($script:playroomBlocks|Where-Object{$_.Type-notin@('Ladder','Wooden','Cloud')-and[Math]::Abs($_.Angle)-gt.01-and-not(Test-LadderBlockPassThrough $p1Ladder $_)})){
$resolved=Resolve-PlayerRotatedBlock $rotatedBlock $script:x $script:y $script:vx $script:vy
if($null-ne$resolved){
$script:x=$resolved.X;$script:y=$resolved.Y;$script:vx=$resolved.VX;$script:vy=$resolved.VY
if($resolved.Grounded){
$script:grounded=$true;$landingBlock=$rotatedBlock;$script:groundPlatformHwnd=[IntPtr]::Zero;$script:groundPlatformRect=$null
$script:surfaceEffect=$rotatedBlock.Type;$script:surfaceEffectTicks=8
if($rotatedBlock.Type-eq'Bouncy'){
$angle=$rotatedBlock.Angle*[Math]::PI/180
$script:vx+=[Math]::Sin($angle)*20.5;$script:vy-=[Math]::Cos($angle)*20.5;$script:grounded=$false
}elseif($rotatedBlock.Type-eq'Fire'){
$angle=$rotatedBlock.Angle*[Math]::PI/180
$script:vx+=[Math]::Sin($angle)*13;$script:vy-=[Math]::Cos($angle)*13;$script:grounded=$false;$script:attackTicks=20
}elseif($rotatedBlock.Type-eq'Conveyor'){
$angle=$rotatedBlock.Angle*[Math]::PI/180
$script:vx=[Math]::Max(-13,[Math]::Min(13,$script:vx+([Math]::Cos($angle)*$rotatedBlock.Direction*.72)))
$script:vy+=[Math]::Sin($angle)*$rotatedBlock.Direction*.72
}
}
if($resolved.Wall-ne0){$script:wallContact=$resolved.Wall;$script:wallGraceTicks=6}
}
}
foreach($wood in @($script:playroomBlocks|Where-Object{$_.Type-in@('Wooden','Cloud')})){
$woodLanding=Resolve-PlayerWoodenPlatform $wood $oldX $oldY $script:x $script:y $script:vx $script:vy $script:dropTicks
if($null-ne$woodLanding){
$script:x=$woodLanding.X;$script:y=$woodLanding.Y;$script:vx=$woodLanding.VX;$script:vy=$woodLanding.VY
if($woodLanding.Grounded){$script:grounded=$true;$landingBlock=$wood;$script:groundPlatformHwnd=[IntPtr]::Zero;$script:groundPlatformRect=$null;$script:surfaceEffect=$wood.Type;$script:surfaceEffectTicks=8;if($wood.Type-eq'Cloud'){Trigger-TemporaryBlock $wood 'Player1'}}
}
}
if ($script:surfaceEffectTicks -gt 0) { $script:surfaceEffectTicks-- } else { $script:surfaceEffect = 'Normal' }
if ($script:grounded) {
$script:coyoteTicks = 6; $script:airJumpsRemaining = 1
} elseif ($script:coyoteTicks -gt 0) {
$script:coyoteTicks--
}
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and-not$script:diagnosticMainMarker){[Console]::Out.WriteLine('diagnostic:main-update-reached');$script:diagnosticMainMarker=$true}
try{Update-Shield}catch{if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:shield-update-error $($_.Exception.ToString())")}}
try{Update-PlayroomBalls}catch{if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:ball-update-error $($_.Exception.ToString())")}}
try{Update-PlayroomGadgets}catch{if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:gadget-update-error $($_.Exception.ToString())")}}
try{Update-PlayroomHazards}catch{if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:hazard-update-error $($_.Exception.ToString())")}}
try{Update-Football}catch{if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:football-update-error $($_.Exception.ToString())")}}
try{Update-Wispfall}catch{if($env:WINDOWISP_DIAGNOSTICS-eq'1'){[Console]::Out.WriteLine("diagnostic:wispfall-update-error $($_.Exception.ToString())")}}
if(-not$script:wispfallActive){try{Update-TemporaryBlocks}catch{}}
foreach($supportCase in @($script:diagnosticSupportCases|Where-Object{-not$_.Complete})){
$supportCase.Ticks++
$supportObject=$supportCase.Object;$supportBlock=$supportCase.Block
$bodyBottom=$supportObject.Y+$supportObject.BodyOffsetY+$supportObject.BodyHeight
if(($supportCase.Ticks%20)-eq0){
[Console]::Out.WriteLine("diagnostic:support-sample case=$($supportCase.Name) tick=$($supportCase.Ticks) bodyBottom=$([Math]::Round($bodyBottom,2)) blockTop=$([Math]::Round($supportBlock.Y,2)) vy=$([Math]::Round($supportObject.VY,2))")
}
if($supportCase.Ticks-ge180){
$resting=([Math]::Abs($bodyBottom-$supportBlock.Y)-le2)-and([Math]::Abs($supportObject.VY)-le0.5)
$fellThrough=($supportObject.Y+$supportObject.BodyOffsetY)-gt($supportBlock.Y+$supportBlock.Height)
$result=if($resting){'PASS'}elseif($fellThrough){'FAIL_FELL_THROUGH'}else{'FAIL_UNSETTLED'}
[Console]::Out.WriteLine("diagnostic:support-result case=$($supportCase.Name) result=$result bodyBottom=$([Math]::Round($bodyBottom,2)) blockTop=$([Math]::Round($supportBlock.Y,2))")
$supportCase.Complete=$true
}
}
Update-Player2
if($env:WINDOWISP_DIAGNOSTICS-eq'1'-and$script:p2ControllerMode-ne'Human'-and($script:inputTick%60)-eq0){
[Console]::Out.WriteLine("diagnostic:p2-ai mode=$($script:p2ControllerMode) action=$($script:p2AIIntent.Label) x=$([Math]::Round($script:p2X,1)) vx=$([Math]::Round($script:p2VX,1)) grounded=$($script:p2Grounded)")
}
Update-HitProtectionVisuals
foreach ($shot in @($script:projectiles)) {
$shot.X += $shot.VX; $shot.Y += $shot.VY; $shot.Life--
if(($script:inputTick%2)-eq0){Set-SharedOverlayElementPosition $shot.Element $shot.X $shot.Y}
$removeShot = $false
if ($shot.Owner -like 'Player*' -and $script:enemies.Count -gt 0) {
foreach ($enemyObject in @($script:enemies)) {
$hitEnemy = ($shot.X + ($shot.Width * 0.9) -gt $enemyObject.X) -and ($shot.X + 5 -lt $enemyObject.X + $enemyObject.Width) -and
($shot.Y + ($shot.Height * 0.9) -gt $enemyObject.Y) -and ($shot.Y + 4 -lt $enemyObject.Y + $enemyObject.Height)
if ($hitEnemy) {
$enemyObject.Health -= $shot.Damage; $removeShot = $true
if ($enemyObject.Health -le 0) { Remove-Enemy $enemyObject $true }
break
}
}
} elseif ($shot.Owner -eq 'Enemy' -and $script:shieldTicks -gt 0) {
$shieldHit = ($shot.X + $shot.Width -gt $script:x - 12) -and ($shot.X -lt $script:x + $PetWidth + 12) -and
($shot.Y + $shot.Height -gt $script:y - 12) -and ($shot.Y -lt $script:y + $PetHeight + 12)
if ($shieldHit) { $removeShot = $true }
} elseif ($shot.Owner -eq 'Enemy' -and $script:twoPlayerActive -and $script:p2InvulnerableTicks -le 0 -and
($shot.X + $shot.Width -gt $script:p2X + 8) -and ($shot.X + 5 -lt $script:p2X + $PetWidth - 8) -and
($shot.Y + $shot.Height -gt $script:p2Y + 8) -and ($shot.Y + 4 -lt $script:p2Y + $PetHeight - 5)) {
$script:p2Hearts--; $script:p2InvulnerableTicks=90; $removeShot=$true
} elseif ($shot.Owner -eq 'Enemy' -and $script:invulnerableTicks -le 0) {
$hitPlayer = ($shot.X + $shot.Width -gt $script:x + 8) -and ($shot.X + 5 -lt $script:x + $PetWidth - 8) -and
($shot.Y + $shot.Height -gt $script:y + 8) -and ($shot.Y + 4 -lt $script:y + $PetHeight - 5)
if ($hitPlayer) {
$script:hearts--; $script:invulnerableTicks = 90; $removeShot = $true
if ($script:hearts -le 0) {
$script:active = $false; Register-ControlKeys $false; Set-ClickThrough $true
Remove-AllEnemies; Remove-Star $false; Remove-Shield; Remove-ShieldPickup
Update-Menu
}
}
}
if ($removeShot -or $shot.Life -le 0 -or $shot.X -lt ($leftBound - 100) -or $shot.X -gt ($rightBound + $PetWidth + 100) -or $shot.Y -lt ($topBound - 100) -or $shot.Y -gt ($floor + 100)) {
Remove-SharedGameplayElement $shot.Element;$script:projectiles.Remove($shot)
}
}
if ($null -ne $script:star -and $script:active -and $script:hearts -gt 0) {
$touchesStar = ($script:x + $PetWidth - 6 -gt $script:star.X + 8) -and ($script:x + 6 -lt $script:star.X + 96) -and
($script:y + $PetHeight - 6 -gt $script:star.Y + 8) -and ($script:y + 6 -lt $script:star.Y + 96)
if ($touchesStar) { Remove-Star $true }
}
if ($null -ne $script:shieldPickup -and $script:active -and $script:hearts -gt 0) {
$touchesShield = ($script:x + $PetWidth - 6 -gt $script:shieldPickup.X + 8) -and ($script:x + 6 -lt $script:shieldPickup.X + 96) -and
($script:y + $PetHeight - 6 -gt $script:shieldPickup.Y + 8) -and ($script:y + 6 -lt $script:shieldPickup.Y + 104)
if ($touchesShield) {
Remove-ShieldPickup
$script:shieldTicks = 480
}
}
$window.Left = $script:x; $window.Top = $script:y
Update-FamiliarBubble
# Commit held toys only after both player simulations and windows have moved.
# This prevents top-level toy windows alternating between adjacent frames.
Update-CarriedToyPositions
$state = if ($script:charging -or $script:attackTicks -gt 0) { 'attack' } elseif (-not $script:grounded) { 'jump' } elseif ([Math]::Abs($script:vx) -gt 0.5) { 'run' } else { 'idle' }
if ($state -ne $script:lastState) { $script:animationTick = 0; $script:lastState = $state } else { $script:animationTick++ }
$frame = if ($state -eq 'attack') {
$(if ($script:charging) { [Math]::Min(3, [Math]::Floor($script:chargeTicks / 23)) } else { [Math]::Min(3, [Math]::Floor($script:animationTick / 3)) })
} elseif ($state -eq 'run') {
[Math]::Floor($script:animationTick / 7) % 8
} elseif ($state -eq 'jump') {
if ($script:grounded) { 4 } elseif ($script:vy -lt -8) { 1 } elseif ($script:vy -lt 2) { 2 } elseif ($script:vy -lt 12) { 3 } else { 4 }
} else {
[Math]::Floor($script:animationTick / 10) % 6
}
Draw-Pet $script:active $state $script:facing ([int]$frame)
$script:hudRefreshTick++
if ($script:hudRefreshTick -ge 6) {
$script:hudRefreshTick = 0
Update-Hud
}
})
$window.Add_Closed({
$timer.Stop()
if($script:wispfallActive-or$null-ne$script:wispfallLavaWindow){Remove-WispfallSetup}
if ($script:hwnd -ne [IntPtr]::Zero) {
foreach ($id in 1..12) { [PetNative]::UnregisterHotKey($script:hwnd, $id) | Out-Null }
}
foreach ($shot in @($script:projectiles)) { Remove-SharedGameplayElement $shot.Element }
Remove-AllEnemies
Remove-Star $false
if($null-ne$script:familiarBubbleWindow){try{$script:familiarBubbleWindow.Close()}catch{};$script:familiarBubbleWindow=$null}
Remove-PlatformHighlights
Remove-Shield
Remove-ShieldPickup
Clear-Dodgeballs
if ($null -ne $script:ropeOverlayWindow) { $script:ropeOverlayWindow.Close();$script:ropeOverlayWindow=$null;$script:ropeOverlayCanvas=$null }
if ($null -ne $script:hudWindow) { $script:hudWindow.Close() }
if ($null -ne $script:heartWindow) { $script:heartWindow.Close() }
if ($null -ne $script:p1StatusWindow) { $script:p1StatusWindow.Close() }
if ($null -ne $script:p2StatusWindow) { $script:p2StatusWindow.Close() }
if ($null -ne $script:menuWindow) { $script:menuWindow.Close() }
if ($null -ne $script:menuButtonWindow) { $script:menuButtonWindow.Close() }
if ($null -ne $script:petSelectorWindow) { $script:petSelectorWindow.Close() }
if ($null -ne $script:sandboxTrayWindow) { $script:sandboxTrayWindow.Close() }
if ($null -ne $script:sandboxHotbarWindow) { $script:sandboxHotbarWindow.Close() }
if ($null -ne $script:gridOverlayWindow) { $script:gridOverlayWindow.Close() }
if ($null -ne $script:controlsWindow) { $script:controlsWindow.Close() }
if ($null -ne $script:tutorialWindow) { $script:tutorialWindow.Close() }
if ($script:twoPlayerActive) { Set-TwoPlayerMode $false }
if(-not[string]::IsNullOrWhiteSpace($script:gadgetBenchmarkPhase)-and$script:gadgetBenchmarkWallStartMs-gt0){
$benchProcess=[Diagnostics.Process]::GetCurrentProcess();$benchProcess.Refresh()
$benchWallMs=$script:frameClock.Elapsed.TotalMilliseconds-$script:gadgetBenchmarkWallStartMs
$benchCpuMs=$benchProcess.TotalProcessorTime.TotalMilliseconds-$script:gadgetBenchmarkCpuStartMs
$benchCorePercent=$(if($benchWallMs-gt0){($benchCpuMs/$benchWallMs)*100}else{0})
$benchAvg=$(if($script:frameGapSamples-gt0){$script:frameGapSum/$script:frameGapSamples}else{0})
$benchFinite=-not([double]::IsNaN($script:x)-or[double]::IsInfinity($script:x)-or[double]::IsNaN($script:y)-or[double]::IsInfinity($script:y)-or[double]::IsNaN($script:p2X)-or[double]::IsInfinity($script:p2X)-or[double]::IsNaN($script:p2Y)-or[double]::IsInfinity($script:p2Y))
foreach($benchBall in @($script:playroomBalls)){if([double]::IsNaN($benchBall.X)-or[double]::IsInfinity($benchBall.X)-or[double]::IsNaN($benchBall.Y)-or[double]::IsInfinity($benchBall.Y)){$benchFinite=$false;break}}
$benchMaxRopeExcess=0.0
foreach($benchRope in @($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'})){
$benchA=Get-RopeAttachmentPoint $benchRope.EndA;$benchB=Get-RopeAttachmentPoint $benchRope.EndB
$benchDx=$benchB.X-$benchA.X;$benchDy=$benchB.Y-$benchA.Y
$benchExcess=[Math]::Max(0,[Math]::Sqrt(($benchDx*$benchDx)+($benchDy*$benchDy))-$benchRope.Length)
if($benchExcess-gt$benchMaxRopeExcess){$benchMaxRopeExcess=$benchExcess}
}
[Console]::Out.WriteLine(('diagnostic:gadget-benchmark-result phase={0} wallMs={1:0} cpuMs={2:0} oneCorePct={3:0.0} avgFrameMs={4:0.00} maxFrameMs={5:0.00} samples={6} handles={7} handleDelta={8} finite={9}' -f $script:gadgetBenchmarkPhase,$benchWallMs,$benchCpuMs,$benchCorePercent,$benchAvg,$script:frameGapMax,$script:frameGapSamples,$benchProcess.HandleCount,($benchProcess.HandleCount-$script:gadgetBenchmarkStartHandles),$benchFinite))
if($script:gadgetBenchmarkPhase-in@('Ropes3','Ropes')){[Console]::Out.WriteLine(('diagnostic:rope-benchmark-integrity count={0} maxLengthExcess={1:0.000} finite={2}' -f @($script:playroomGadgets|Where-Object{$_.Kind-eq'Rope'}).Count,$benchMaxRopeExcess,$benchFinite))}
}
if ($script:frameGapSamples -gt 0) {
Write-Output ('diagnostic:frame-pacing avg={0:0.00}ms max={1:0.00}ms samples={2}' -f ($script:frameGapSum / $script:frameGapSamples),$script:frameGapMax,$script:frameGapSamples)
}
if ($script:playroomActive) {
Clear-PlayroomObjects
$script:playroomActive = $false
}
try { $singleInstance.ReleaseMutex() } catch {}
$singleInstance.Dispose()
$gameOnEvent.Dispose(); $gameOffEvent.Dispose(); $gameToggleEvent.Dispose(); $gameExitEvent.Dispose(); $playroomEvent.Dispose(); $petSwitchEvent.Dispose();$familiarEventSignal.Dispose()
})
Draw-Pet $false 'idle' 0 0
if ($env:WINDOWISP_DIAGNOSTICS -eq '1') { Write-Output 'diagnostic:show-dialog' }
$timer.Start()
try{$window.ShowDialog()|Out-Null}
catch{
if($env:WINDOWISP_DIAGNOSTICS-eq'1'){
[Console]::Error.WriteLine("diagnostic:fatal $($_.Exception.ToString())")
[Console]::Error.WriteLine("diagnostic:stack $($_.ScriptStackTrace)")
}
throw
}
SHA-256: 2299309c87fcfc63eaa2250e58bb5e3b308b43e38add005b48be17918376a4e0