Skip to main content
Voltorb Flip is a puzzle game from Pokémon HeartGold/SoulSilver, embedded with advanced iframe communication for focus and volume control.

Features

  • Puzzle Gameplay: Flip tiles to find numbers while avoiding Voltorbs
  • Score Multipliers: Multiply your score by finding higher numbers
  • Level Progression: Advance through increasingly difficult levels
  • Volume Integration: Syncs with global volume control
  • Focus Detection: Pauses when window loses focus
  • PostMessage API: Two-way communication with iframe
  • Screen Size Detection: Blocks on screens smaller than 570x670

Component Structure

Location: src/WinXP/apps/VoltorbFlip/index.jsx
function VoltorbFlip({ isFocus }) {
  const gameUrl = `${import.meta.env.BASE_URL}voltorb_flip/`;
  const iframeRef = useRef(null);
  const [iframeReady, setIframeReady] = useState(false);
  
  const { volume, isMuted } = useVolume();
  
  // PostMessage communication with iframe
}

Configuration

From apps/index.jsx:
VoltorbFlip: {
  name: 'VoltorbFlip',
  header: { icon: voltorbFlipIcon, title: 'Voltorb Flip' },
  component: WrappedVoltorb,
  defaultSize: checkVoltorbBlock()
    ? { width: 380, height: 0 }
    : { width: 570, height: 670 },
  defaultOffset: checkVoltorbBlock()
    ? getCenter(380, 200)
    : getCenter(570, 670),
  resizable: false,
  minimized: false,
  maximized: checkVoltorbBlock() ? false : shouldMaximize(570, 670, false),
  multiInstance: false,
}

Screen Size Detection

Voltorb Flip requires a minimum screen size:
const checkVoltorbBlock = () => isScreenTooSmall(570, 670);

const WrappedVoltorb = props => {
  if (checkVoltorbBlock()) {
    return (
      <ErrorBox
        {...props}
        message="Screen Too Small: Voltorb Flip requires a viewport of at least 570x670px. Please rotate your device or use a larger screen."
        title="Display Error"
      />
    );
  }
  return <VoltorbFlipComponent {...props} />;
};

PostMessage Communication

The game uses PostMessage API for iframe communication:

Getting Target Origin

const getTargetOrigin = useCallback(() => {
  let targetOrigin;
  try {
    targetOrigin = new URL(gameUrl).origin;
  } catch (e) {
    targetOrigin = new URL(gameUrl, window.location.origin).origin;
  }
  if (targetOrigin === 'null' || targetOrigin === 'about:blank') {
    targetOrigin = window.location.origin;
  }
  return targetOrigin;
}, [gameUrl]);

Sending Focus Messages

const sendFocusMessage = useCallback(
  focusState => {
    const iframe = iframeRef.current;
    if (iframe && iframe.contentWindow) {
      const targetOrigin = getTargetOrigin();
      iframe.contentWindow.postMessage(
        { type: 'VOLTORB_FLIP_FOCUS_CHANGE', focused: focusState },
        targetOrigin,
      );
    }
  },
  [getTargetOrigin],
);

useEffect(() => {
  if (iframeReady) {
    sendFocusMessage(isFocus);
  }
}, [isFocus, iframeReady, sendFocusMessage]);

Sending Volume Messages

const sendVolumeMessage = useCallback(
  (currentVolume, currentMuted) => {
    const iframe = iframeRef.current;
    if (iframe && iframe.contentWindow && iframeReady) {
      const targetOrigin = getTargetOrigin();
      iframe.contentWindow.postMessage(
        {
          type: 'VOLTORB_FLIP_VOLUME_CHANGE',
          volume: currentVolume / 100,
          muted: currentMuted,
        },
        targetOrigin,
      );
    }
  },
  [getTargetOrigin, iframeReady],
);

useEffect(() => {
  if (iframeReady) {
    sendVolumeMessage(volume, isMuted);
  }
}, [volume, isMuted, iframeReady, sendVolumeMessage]);

Receiving Ready Message

useEffect(() => {
  const handleIframeMessage = event => {
    const expectedIframeOrigin = getTargetOrigin();
    
    // Validate origin
    if (event.origin !== expectedIframeOrigin) return;
    
    // Validate source
    if (event.source !== iframeRef.current?.contentWindow) return;
    
    // Handle ready message
    if (
      event.data &&
      typeof event.data === 'object' &&
      event.data.type === 'VOLTORB_FLIP_IFRAME_READY'
    ) {
      setIframeReady(true);
      sendFocusMessage(isFocus);
      sendVolumeMessage(volume, isMuted);
    }
  };
  
  window.addEventListener('message', handleIframeMessage);
  return () => {
    window.removeEventListener('message', handleIframeMessage);
  };
}, [gameUrl, isFocus, sendFocusMessage, getTargetOrigin, volume, isMuted, sendVolumeMessage]);

Message Types

From Web XP to Game

Focus Change

{
  type: 'VOLTORB_FLIP_FOCUS_CHANGE',
  focused: boolean // true when window has focus
}

Volume Change

{
  type: 'VOLTORB_FLIP_VOLUME_CHANGE',
  volume: number,  // 0.0 to 1.0
  muted: boolean
}

From Game to Web XP

Ready Message

{
  type: 'VOLTORB_FLIP_IFRAME_READY'
}
This message is sent when the game has loaded and is ready to receive messages.

Game Implementation

The game inside the iframe should implement:
// Inside voltorb_flip iframe
let currentVolume = 1.0;
let isMuted = false;
let isFocused = true;

// Send ready message when loaded
window.parent.postMessage(
  { type: 'VOLTORB_FLIP_IFRAME_READY' },
  window.location.origin
);

// Listen for messages from parent
window.addEventListener('message', event => {
  // Validate origin
  if (event.origin !== window.location.origin) return;
  
  if (event.data.type === 'VOLTORB_FLIP_FOCUS_CHANGE') {
    isFocused = event.data.focused;
    if (!isFocused) {
      pauseGame();
    }
  }
  
  if (event.data.type === 'VOLTORB_FLIP_VOLUME_CHANGE') {
    currentVolume = event.data.volume;
    isMuted = event.data.muted;
    updateAudioVolume();
  }
});

function updateAudioVolume() {
  const effectiveVolume = isMuted ? 0 : currentVolume;
  // Apply to all audio elements
  audioElements.forEach(audio => {
    audio.volume = effectiveVolume;
  });
}

function pauseGame() {
  // Pause game logic when window loses focus
}

Gameplay

Objective

Flip tiles on a 5x5 grid to find numbers (2 or 3) while avoiding Voltorbs (0). Multiply your points by the numbers you find.

Rules

  1. Each row and column shows:
    • Sum of all numbers in that row/column
    • Number of Voltorbs in that row/column
  2. Flip tiles to reveal:
    • 2 or 3: Multiplies your current score
    • 1: No multiplication (safe)
    • Voltorb: Game over, lose all points for this round
  3. Complete a level by finding all 2s and 3s without hitting a Voltorb
  4. Advance to higher levels with more Voltorbs and higher rewards

Strategy

  • Start with rows/columns that have 0 Voltorbs
  • Look for rows with high sums and few Voltorbs
  • Use logic to eliminate impossible tile values
  • Cash out before risking it all on uncertain tiles

Focus Behavior

When the window loses focus:
  • Game automatically pauses
  • Audio mutes (if not already muted)
  • Prevents accidental moves
  • Resumes when window regains focus

Volume Integration

The game volume is controlled by:
  • Global Volume: Master volume from taskbar
  • Local Mute: Mute button in Web XP
  • Effective Volume: globalVolume * (isMuted ? 0 : 1)

Overlay for Unfocused State

const Overlay = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  background-color: transparent;
`;

{!isFocus && <Overlay />}

Styling

const AppContainer = styled.div`
  width: 100%;
  height: 100%;
  position: relative;
  background-color: #309f6a; // Game background color
  overflow: hidden;
`;

const StyledIframe = styled.iframe`
  display: block;
  width: 100%;
  height: 100%;
  border: none;
`;

Usage Example

import { VoltorbFlip } from 'src/WinXP/apps';
import { useVolume } from 'context/VolumeContext';

function Desktop() {
  const [focusedWindow, setFocusedWindow] = useState(null);
  
  return (
    <Window 
      title="Voltorb Flip"
      onFocus={() => setFocusedWindow('voltorb')}
      onBlur={() => setFocusedWindow(null)}
    >
      <VoltorbFlip isFocus={focusedWindow === 'voltorb'} />
    </Window>
  );
}

Security Considerations

The PostMessage implementation includes security checks:
// Validate origin
if (event.origin !== expectedIframeOrigin) return;

// Validate source
if (event.source !== iframeRef.current?.contentWindow) return;

// Validate message structure
if (!event.data || typeof event.data !== 'object') return;
if (!event.data.type) return;

Local Hosting

The game files should be in public/voltorb_flip/:
public/
└── voltorb_flip/
    ├── index.html
    ├── game.js
    ├── style.css
    └── assets/
        ├── images/
        └── sounds/

Error Handling

Handle cases where the iframe fails to load:
const [loadError, setLoadError] = useState(false);
const [loadTimeout, setLoadTimeout] = useState(false);

useEffect(() => {
  const timeout = setTimeout(() => {
    if (!iframeReady) {
      setLoadTimeout(true);
    }
  }, 10000); // 10 second timeout
  
  return () => clearTimeout(timeout);
}, [iframeReady]);

{loadTimeout && (
  <ErrorMessage>
    Game is taking longer than expected to load...
  </ErrorMessage>
)}

Build docs developers (and LLMs) love