Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions examples/battlestation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Battlestation

Battlestation is the heavier Electrobun follow-up after the counter proofs.

It now supports:

- web / Tauri with the original DOM HUD + audio path
- Electrobun `BrowserWindow` with the same web-first path
- Electrobun `GpuWindow` with the shared mission loop and native Three WebGPU rendering

## Run

BrowserWindow shell:

```bash
bun run dev:electrobun:browser
```

GPUWindow shell:

```bash
bun run dev:electrobun:gpu
```

## Current GPUWindow limitations

The GPUWindow path is intentionally narrower than the browser shell:

- no HTML HUD overlay in native mode
- no Web Audio tone playback in native mode yet
- mission status is surfaced through the window title and console alerts
- keyboard + mouse controls are implemented directly through Electrobun window/screen APIs
- native title-bar chrome may slightly skew top-edge mouse targeting until Electrobun exposes content-area coordinates

That split is deliberate: the mission orchestration is now shared, while rendering/input are runtime-specific adapters.

## Build

```bash
bun run build:web
bun run build:electrobun:browser
bun run build:electrobun:gpu
```
25 changes: 22 additions & 3 deletions examples/battlestation/electrobun.config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import type { ElectrobunConfig } from "electrobun";

const renderMode = process.env.GAMETAU_ELECTROBUN_RENDER_MODE === "gpu"
? "gpu"
: "browser";
const isGpu = renderMode === "gpu";

export default {
app: {
name: "Battlestation (Electrobun Showcase)",
name: isGpu ? "Battlestation (GPUWindow)" : "Battlestation (Electrobun Showcase)",
identifier: "dev.gametau.battlestation.showcase",
version: "0.1.0",
},
build: {
bun: {
entrypoint: "src/bun/index.js",
entrypoint: isGpu ? "src/bun/gpu.ts" : "src/bun/browser.ts",
},
copy: {
"dist/index.html": "views/main/index.html",
"dist/assets": "views/main/assets",
},
mac: {
bundleCEF: !isGpu,
bundleWGPU: isGpu,
},
linux: {
bundleCEF: !isGpu,
bundleWGPU: isGpu,
},
win: {
bundleCEF: !isGpu,
bundleWGPU: isGpu,
},
},
};
} satisfies ElectrobunConfig;
12 changes: 9 additions & 3 deletions examples/battlestation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,29 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev:electrobun": "concurrently -k -n WEB,APP \"vite\" \"electrobun dev\"",
"dev:electrobun": "bun run dev:electrobun:browser",
"dev:electrobun:browser": "concurrently -k -n WEB,APP \"vite\" \"node ./node_modules/electrobun/bin/electrobun.cjs dev\"",
"dev:electrobun:gpu": "cross-env GAMETAU_ELECTROBUN_RENDER_MODE=gpu node ./node_modules/electrobun/bin/electrobun.cjs dev",
"dev:tauri": "tauri dev",
"build:web": "vite build",
"build:electrobun": "bun run build:web && electrobun build",
"build:electrobun": "bun run build:electrobun:browser",
"build:electrobun:browser": "bun run build:web && node ./node_modules/electrobun/bin/electrobun.cjs build",
"build:electrobun:gpu": "bun run build:web && cross-env GAMETAU_ELECTROBUN_RENDER_MODE=gpu node ./node_modules/electrobun/bin/electrobun.cjs build",
"build:desktop": "tauri build",
"preview": "vite preview"
},
"dependencies": {
"electrobun": "^1.14.4",
"electrobun": "^1.15.1",
"three": "^0.172.0",
"webtau": "workspace:*"
},
"devDependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/cli": "^2.0.0",
"@types/bun": "^1.3.9",
"@types/three": "^0.172.0",
"concurrently": "^9.2.1",
"cross-env": "^7.0.3",
"typescript": "^5.8.0",
"vite": "^6.0.0",
"webtau-vite": "workspace:*"
Expand Down
9 changes: 9 additions & 0 deletions examples/battlestation/src/bun/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { BrowserWindow } from "electrobun/bun";

const isProduction = Bun.env.NODE_ENV === "production";
const url = isProduction ? "views://main/index.html" : "http://localhost:1420";

new BrowserWindow({
title: "gametau battlestation (electrobun showcase)",
url,
});
141 changes: 141 additions & 0 deletions examples/battlestation/src/bun/gpu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { configure } from "webtau";
import { GpuWindow, Screen } from "electrobun/bun";
import { FALLBACK_MISSION, FALLBACK_THEME } from "../game/config";
import { createDefenseSceneGpu } from "../game/scene-gpu";
import { startBattlestationRuntime } from "../game/runtime";

// Mixed JS/native key codes while Electrobun's Bun-side keyboard surface settles:
// 37/39 = ArrowLeft/ArrowRight, 65/68 = A/D, 123/124 = native left/right on macOS,
// 13/32 = Enter/Space, 77 = M.
const LEFT_KEYS = new Set([37, 65, 123]);
const RIGHT_KEYS = new Set([39, 68, 124]);
const FIRE_KEYS = new Set([13, 32]);
const MUTE_KEYS = new Set([77]);

function createSilentAudioAdapter() {
return {
setMasterVolume() {},
setMuted() {},
async resume() {},
async playTone() {},
};
}

async function loadRuntime() {
configure({
loadWasm: async () => {
const wasm = await import("../wasm/battlestation_wasm");
await wasm.default();
wasm.init();
return wasm;
},
});
}

async function main() {
await loadRuntime();

const window = new GpuWindow({
title: "A130 Defense (GPUWindow)",
frame: { x: 120, y: 120, width: 960, height: 960 },
titleBarStyle: "default",
transparent: false,
});

const pressedKeys = new Set<number>();
let mouseLatch = false;
let lastProfile = "runs 0 / best 0";
let lastAlert = "Stand by.";

window.on("keyDown", (event) => {
const data = event as { data?: { keyCode?: number } };
if (typeof data.data?.keyCode === "number") {
pressedKeys.add(data.data.keyCode);
}
});
window.on("keyUp", (event) => {
const data = event as { data?: { keyCode?: number } };
if (typeof data.data?.keyCode === "number") {
pressedKeys.delete(data.data.keyCode);
}
});

const scene = createDefenseSceneGpu(window, FALLBACK_THEME.scene);

const stop = await startBattlestationRuntime({
mission: FALLBACK_MISSION,
theme: FALLBACK_THEME,
scene,
audio: createSilentAudioAdapter(),
controls: {
getSelectionAxis() {
if ([...LEFT_KEYS].some((key) => pressedKeys.has(key))) return -1;
if ([...RIGHT_KEYS].some((key) => pressedKeys.has(key))) return 1;
return 0;
},
getFirePressed() {
return [...FIRE_KEYS].some((key) => pressedKeys.has(key));
},
getMutePressed() {
return [...MUTE_KEYS].some((key) => pressedKeys.has(key));
},
drainPointerTargets() {
const buttons = Screen.getMouseButtons();
const leftDown = (buttons & 1n) === 1n;
const targets: Array<{ x: number; y: number }> = [];

if (leftDown && !mouseLatch) {
const cursor = Screen.getCursorScreenPoint();
const frame = window.getFrame();
// Electrobun currently exposes the outer window frame here, not an
// explicit content-rect API. That means native chrome can slightly
// skew top-edge click mapping until a content-area coordinate surface
// is available on the Bun side.
const inside = cursor.x >= frame.x
&& cursor.x <= frame.x + frame.width
&& cursor.y >= frame.y
&& cursor.y <= frame.y + frame.height;
if (inside && frame.width > 0 && frame.height > 0) {
targets.push({
x: ((cursor.x - frame.x) / frame.width) * 640,
y: ((cursor.y - frame.y) / frame.height) * 640,
});
}
}

mouseLatch = leftDown;
return targets;
},
dispose() {},
},
hud: {
updateMission(view) {
const target = view.selected_contact_id === null ? "NONE" : `#${view.selected_contact_id}`;
window.setTitle(
`A130 Defense | ${view.mission_state} | score ${view.score} | integrity ${view.integrity} | target ${target}`,
);
},
updateProfile(profile) {
lastProfile = `runs ${profile.missionsRun} / best ${profile.bestScore}`;
},
setAlert(text) {
lastAlert = text;
console.log(text);
},
setAlertLog(lines) {
if (lines.length > 0) {
console.log(lines[0]);
}
},
setStatus(text) {
window.setTitle(`${text} | ${lastProfile} | ${lastAlert}`);
},
},
});

window.on("close", () => {
stop();
});
}

main().catch(console.error);
10 changes: 1 addition & 9 deletions examples/battlestation/src/bun/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1 @@
import { BrowserWindow } from "electrobun/bun";

const isProduction = process.env.NODE_ENV === "production";
const url = isProduction ? "views://main/index.html" : "http://localhost:1420";

new BrowserWindow({
title: "gametau battlestation (electrobun showcase)",
url,
});
import "./browser";
59 changes: 59 additions & 0 deletions examples/battlestation/src/game/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export interface MissionStubConfig {
callsign: string;
sector: string;
objective: string;
intel: string;
tacticalProtocol: string[];
}

export interface ThemeConfig {
scene: SceneTheme;
audio: {
hitHz: number;
killConfirmHz: number;
missHz: number;
integrityLossHz: number;
criticalAlertHz: number;
};
ui: {
criticalIntegrityThreshold: number;
};
}

export interface SceneTheme {
background: string;
grid: string;
selected: string;
shipColor: string;
friendlyColor: string;
contactPulseSpeed?: number;
contactPulseAmount?: number;
}

export const FALLBACK_MISSION: MissionStubConfig = {
callsign: "LANCE-130",
sector: "Outer Grid Delta-7",
objective: "Defend friendly cluster from approaching hostiles.",
intel: "No mission config found. Using fallback profile.",
tacticalProtocol: [],
};

export const FALLBACK_THEME: ThemeConfig = {
scene: {
background: "#030608",
grid: "#1a3040",
selected: "#ffe680",
shipColor: "#00e5ff",
friendlyColor: "#22cc44",
},
audio: {
hitHz: 660,
killConfirmHz: 880,
missHz: 220,
integrityLossHz: 160,
criticalAlertHz: 120,
},
ui: {
criticalIntegrityThreshold: 25,
},
};
Loading