Skip to content

Development

BiosSystem edited this page May 22, 2026 · 1 revision

Development

This guide covers the full developer workflow: environment setup, project architecture, Tauri v2 internals, the Phaser 4 scene system, and step-by-step instructions for adding a new game.


Prerequisites

Tool Version Purpose
Node.js 20 LTS JavaScript runtime & npm
Rust 1.77.2+ Tauri native shell compilation
Cargo Bundled with Rust Rust package manager
Vite 8.0.14 (devDep) Frontend bundler / HMR dev server
TypeScript ~6.0.3 Type-safe game logic
Tauri CLI 2.11.2 (devDep) Native app packaging

Install Rust via rustup: https://rustup.rs

For Android cross-compilation:

rustup target add aarch64-linux-android
# Also requires Android NDK via Android Studio

Repository Structure

retro-game-replicas/
├── index.html                  # Vite entry point (mounts #app div)
├── package.json                # npm scripts, deps (Phaser 4, Tauri API)
├── tsconfig.json               # TypeScript config (strict mode)
├── vite.config.ts              # Vite build config (if present)
│
├── src/
│   ├── main.ts                 # Phaser game config + scene registry
│   ├── style.css               # Global styles (body, canvas centering)
│   ├── counter.ts              # (Legacy utility, not used in games)
│   └── scenes/
│       ├── LobbyScene.ts       # Main menu, game selector, CRT toggle
│       ├── SnakeScene.ts
│       ├── PongScene.ts
│       ├── AsteroidsScene.ts
│       ├── BreakoutScene.ts
│       ├── FroggerScene.ts
│       ├── InvadersScene.ts
│       ├── TetrisScene.ts
│       ├── MinesweeperScene.ts
│       ├── RunnerScene.ts
│       ├── BirdScene.ts
│       └── CyberScene.ts
│
├── src-tauri/
│   ├── tauri.conf.json         # App metadata, window size, bundle targets
│   ├── Cargo.toml              # Rust dependencies (tauri 2.11.1)
│   ├── build.rs                # Tauri build script
│   ├── src/
│   │   └── lib.rs              # Rust app entry (tauri::Builder)
│   ├── capabilities/           # Tauri v2 permission system
│   └── icons/                  # App icons (32x32, 128x128, .icns, .ico)
│
├── public/                     # Static assets (served as-is by Vite)
├── dist/                       # Vite build output (consumed by Tauri)
└── docs/
    └── images/                 # Screenshots used in README

Technology Deep Dive

Tauri v2

Tauri v2 replaces the traditional Electron model (bundled Chromium) with the system WebView:

  • Windows: Microsoft Edge WebView2 (Chromium-based, always updated via Windows Update)
  • macOS: WKWebView (WebKit, part of the OS)
  • Android: Android WebView (Chromium-based)

This makes the binary dramatically smaller (~15 MB vs. Electron's ~100+ MB) at the cost of minor cross-platform rendering differences (mitigated because Phaser uses a WebGL canvas rather than DOM rendering).

The Rust backend (src-tauri/src/lib.rs) in this project is minimal — it simply boots the Tauri window and loads the Vite-built frontend. No custom Tauri commands are registered.

Key config (src-tauri/tauri.conf.json):

{
  "productName": "Retro Arcade Launcher",
  "version": "1.0.2",
  "identifier": "com.biossystem.retroarcade",
  "build": {
    "frontendDist": "../dist",
    "devUrl": "http://localhost:5173",
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build"
  },
  "app": {
    "windows": [{
      "title": "Retro Arcade Launcher",
      "width": 800,
      "height": 600,
      "resizable": true
    }]
  }
}

Phaser 4

Phaser 4 is the game engine. It provides:

  • WebGL renderer (with Canvas fallback via Phaser.AUTO)
  • Scene system for game-state management
  • Arcade Physics (AABB + circle) for collision detection
  • Input plugins for keyboard, mouse, touch, and gamepad
  • GameObjects (Graphics, Text, Container, Particles, Rectangle)
  • PostFX camera pipeline for the CRT shader

The game is configured in src/main.ts as a single Phaser.Game instance at 640×480 logical pixels, scaled to fit the window with Phaser.Scale.FIT:

const config: Phaser.Types.Core.GameConfig = {
  type: Phaser.AUTO,        // WebGL preferred, Canvas fallback
  width: 640,
  height: 480,
  scale: {
    mode: Phaser.Scale.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH
  },
  backgroundColor: '#0a0a0a',
  pixelArt: true,           // Disables anti-aliasing for crisp pixels
  physics: {
    default: 'arcade',
    arcade: { debug: false }
  },
  scene: [LobbyScene, SnakeScene, PongScene, ...]
};

Scene Lifecycle

Each game is a Phaser Scene subclass with three key lifecycle methods:

class MyGameScene extends Phaser.Scene {
  constructor() { super('MyGameScene'); }

  // Called once when scene starts. Receives data from scene.start(key, data).
  create(data: { difficulty: string }) { ... }

  // Called every frame (~60fps). delta = ms since last frame.
  update(time: number, delta: number) { ... }
}

Scenes transition via:

  • this.scene.start('TargetScene', { difficulty: 'NORMAL' }) — stops current, starts target
  • this.scene.restart({ difficulty: this.difficulty }) — restarts current scene with data

Difficulty System

All games receive a difficulty string ('EASY' | 'NORMAL' | 'HARD' | 'EXPERT') via the scene data object. Each scene is responsible for tuning its own parameters:

create(data: any) {
  this.difficulty = data?.difficulty || 'NORMAL';
  switch (this.difficulty) {
    case 'EASY':   this.speed = 80;  break;
    case 'NORMAL': this.speed = 120; break;
    case 'HARD':   this.speed = 160; break;
    case 'EXPERT': this.speed = 210; break;
  }
}

High Score Persistence

Scores are stored in localStorage with the pattern:

arcade_score_<SceneName>_<DIFFICULTY>

Example keys:

  • arcade_score_SnakeScene_NORMAL"240"
  • arcade_score_TetrisScene_EXPERT"5600"

To save a high score:

const key = `arcade_score_${this.scene.key}_${this.difficulty}`;
const prev = parseInt(localStorage.getItem(key) || '0');
if (this.score > prev) {
  localStorage.setItem(key, String(this.score));
}

The lobby displays the EASY-difficulty high score for quick reference beside each game entry.


Development Workflow

# 1. Install dependencies
npm install

# 2. Start the Tauri dev environment
#    - Starts Vite HMR server on localhost:5173
#    - Opens native Tauri window pointing at it
npm run tauri dev

# 3. TypeScript type checking (without building)
npx tsc --noEmit

# 4. Production build
npm run tauri build

Vite's HMR (Hot Module Replacement) is active during tauri dev — editing any .ts or .css file in src/ will hot-reload the frontend without restarting the Rust process.


Adding a New Game

Follow these steps to add a new game called "Neon Chess" (scene key: ChessScene) as an example.

Step 1: Create the Scene File

# Create the file
touch src/scenes/ChessScene.ts
// src/scenes/ChessScene.ts
import Phaser from 'phaser';

export default class ChessScene extends Phaser.Scene {
  private difficulty = 'NORMAL';

  constructor() {
    super('ChessScene'); // Must be unique across all scenes
  }

  create(data: any) {
    this.difficulty = data?.difficulty || 'NORMAL';

    // Add your game UI, objects, physics, input...
    this.add.text(320, 240, 'NEON CHESS', {
      fontFamily: 'Courier',
      fontSize: '32px',
      color: '#00ffcc'
    }).setOrigin(0.5);

    // Always provide an ESC back to lobby
    this.input.keyboard?.on('keydown-ESC', () => {
      this.scene.start('LobbyScene');
    });
  }

  update(_time: number, delta: number) {
    // Game loop...
  }
}

Step 2: Register in main.ts

// src/main.ts
import ChessScene from './scenes/ChessScene'; // Add import

const config: Phaser.Types.Core.GameConfig = {
  // ...
  scene: [
    LobbyScene,
    SnakeScene,
    // ... existing scenes ...
    ChessScene  // ← Add here
  ]
};

Step 3: Add to the Lobby Game List

In src/scenes/LobbyScene.ts, add an entry to the games array:

private games = [
  // ... existing games ...
  { name: 'NEON CHESS', scene: 'ChessScene', icon: '♟️' },
];

The lobby will automatically display it in the list, read its high score from localStorage, and pass the selected difficulty to ChessScene.create().

Step 4: Test

npm run tauri dev

Navigate to your new game in the lobby, select a difficulty, and verify it launches correctly. Press Esc to confirm the lobby return works.


Code Style Guidelines

  • TypeScript strict mode is enabled — avoid any where possible.
  • Use private access modifiers for all scene properties.
  • Initialize all Phaser GameObjects in create(), not the constructor.
  • Always clean up keyboard listeners in create() before re-adding:
    this.input.keyboard?.removeAllListeners();
  • Use Phaser.Math.Wrap() for wrapping indices (e.g., menu navigation).
  • Prefer this.scene.start() over this.scene.switch() for clean state resets.
  • Store difficulty in this.difficulty and pass it to scene.restart() on game over.

GitHub Actions / CI

The repository includes a .github/ directory with workflow files for automated builds. On every push to main, the CI:

  1. Installs Node.js dependencies (npm ci)
  2. Builds the Vite frontend (npm run build)
  3. Runs tauri build for Windows (x64), macOS (arm64, x64), and Android
  4. Uploads artifacts to the GitHub Release

Back to Home

Clone this wiki locally