Skip to content

Commit a35dcd7

Browse files
authored
Merge pull request #167 from devallibus/feat/battlestation-gpuwindow-runtime-split
Split Battlestation runtime for GPUWindow
2 parents fb17d65 + 81eb992 commit a35dcd7

16 files changed

Lines changed: 701 additions & 304 deletions

bun.lock

Lines changed: 9 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/battlestation/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Battlestation
2+
3+
Battlestation is the heavier Electrobun follow-up after the counter proofs.
4+
5+
It now supports:
6+
7+
- web / Tauri with the original DOM HUD + audio path
8+
- Electrobun `BrowserWindow` with the same web-first path
9+
- Electrobun `GpuWindow` with the shared mission loop and native Three WebGPU rendering
10+
11+
## Run
12+
13+
BrowserWindow shell:
14+
15+
```bash
16+
bun run dev:electrobun:browser
17+
```
18+
19+
GPUWindow shell:
20+
21+
```bash
22+
bun run dev:electrobun:gpu
23+
```
24+
25+
## Current GPUWindow limitations
26+
27+
The GPUWindow path is intentionally narrower than the browser shell:
28+
29+
- no HTML HUD overlay in native mode
30+
- no Web Audio tone playback in native mode yet
31+
- mission status is surfaced through the window title and console alerts
32+
- keyboard + mouse controls are implemented directly through Electrobun window/screen APIs
33+
- native title-bar chrome may slightly skew top-edge mouse targeting until Electrobun exposes content-area coordinates
34+
35+
That split is deliberate: the mission orchestration is now shared, while rendering/input are runtime-specific adapters.
36+
37+
## Build
38+
39+
```bash
40+
bun run build:web
41+
bun run build:electrobun:browser
42+
bun run build:electrobun:gpu
43+
```
Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
1+
import type { ElectrobunConfig } from "electrobun";
2+
3+
const renderMode = process.env.GAMETAU_ELECTROBUN_RENDER_MODE === "gpu"
4+
? "gpu"
5+
: "browser";
6+
const isGpu = renderMode === "gpu";
7+
18
export default {
29
app: {
3-
name: "Battlestation (Electrobun Showcase)",
10+
name: isGpu ? "Battlestation (GPUWindow)" : "Battlestation (Electrobun Showcase)",
411
identifier: "dev.gametau.battlestation.showcase",
512
version: "0.1.0",
613
},
714
build: {
815
bun: {
9-
entrypoint: "src/bun/index.js",
16+
entrypoint: isGpu ? "src/bun/gpu.ts" : "src/bun/browser.ts",
1017
},
1118
copy: {
1219
"dist/index.html": "views/main/index.html",
1320
"dist/assets": "views/main/assets",
1421
},
22+
mac: {
23+
bundleCEF: !isGpu,
24+
bundleWGPU: isGpu,
25+
},
26+
linux: {
27+
bundleCEF: !isGpu,
28+
bundleWGPU: isGpu,
29+
},
30+
win: {
31+
bundleCEF: !isGpu,
32+
bundleWGPU: isGpu,
33+
},
1534
},
16-
};
35+
} satisfies ElectrobunConfig;

examples/battlestation/package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,29 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
8-
"dev:electrobun": "concurrently -k -n WEB,APP \"vite\" \"electrobun dev\"",
8+
"dev:electrobun": "bun run dev:electrobun:browser",
9+
"dev:electrobun:browser": "concurrently -k -n WEB,APP \"vite\" \"node ./node_modules/electrobun/bin/electrobun.cjs dev\"",
10+
"dev:electrobun:gpu": "cross-env GAMETAU_ELECTROBUN_RENDER_MODE=gpu node ./node_modules/electrobun/bin/electrobun.cjs dev",
911
"dev:tauri": "tauri dev",
1012
"build:web": "vite build",
11-
"build:electrobun": "bun run build:web && electrobun build",
13+
"build:electrobun": "bun run build:electrobun:browser",
14+
"build:electrobun:browser": "bun run build:web && node ./node_modules/electrobun/bin/electrobun.cjs build",
15+
"build:electrobun:gpu": "bun run build:web && cross-env GAMETAU_ELECTROBUN_RENDER_MODE=gpu node ./node_modules/electrobun/bin/electrobun.cjs build",
1216
"build:desktop": "tauri build",
1317
"preview": "vite preview"
1418
},
1519
"dependencies": {
16-
"electrobun": "^1.14.4",
20+
"electrobun": "^1.15.1",
1721
"three": "^0.172.0",
1822
"webtau": "workspace:*"
1923
},
2024
"devDependencies": {
2125
"@tauri-apps/api": "^2.0.0",
2226
"@tauri-apps/cli": "^2.0.0",
27+
"@types/bun": "^1.3.9",
2328
"@types/three": "^0.172.0",
2429
"concurrently": "^9.2.1",
30+
"cross-env": "^7.0.3",
2531
"typescript": "^5.8.0",
2632
"vite": "^6.0.0",
2733
"webtau-vite": "workspace:*"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { BrowserWindow } from "electrobun/bun";
2+
3+
const isProduction = Bun.env.NODE_ENV === "production";
4+
const url = isProduction ? "views://main/index.html" : "http://localhost:1420";
5+
6+
new BrowserWindow({
7+
title: "gametau battlestation (electrobun showcase)",
8+
url,
9+
});
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { configure } from "webtau";
2+
import { GpuWindow, Screen } from "electrobun/bun";
3+
import { FALLBACK_MISSION, FALLBACK_THEME } from "../game/config";
4+
import { createDefenseSceneGpu } from "../game/scene-gpu";
5+
import { startBattlestationRuntime } from "../game/runtime";
6+
7+
// Mixed JS/native key codes while Electrobun's Bun-side keyboard surface settles:
8+
// 37/39 = ArrowLeft/ArrowRight, 65/68 = A/D, 123/124 = native left/right on macOS,
9+
// 13/32 = Enter/Space, 77 = M.
10+
const LEFT_KEYS = new Set([37, 65, 123]);
11+
const RIGHT_KEYS = new Set([39, 68, 124]);
12+
const FIRE_KEYS = new Set([13, 32]);
13+
const MUTE_KEYS = new Set([77]);
14+
15+
function createSilentAudioAdapter() {
16+
return {
17+
setMasterVolume() {},
18+
setMuted() {},
19+
async resume() {},
20+
async playTone() {},
21+
};
22+
}
23+
24+
async function loadRuntime() {
25+
configure({
26+
loadWasm: async () => {
27+
const wasm = await import("../wasm/battlestation_wasm");
28+
await wasm.default();
29+
wasm.init();
30+
return wasm;
31+
},
32+
});
33+
}
34+
35+
async function main() {
36+
await loadRuntime();
37+
38+
const window = new GpuWindow({
39+
title: "A130 Defense (GPUWindow)",
40+
frame: { x: 120, y: 120, width: 960, height: 960 },
41+
titleBarStyle: "default",
42+
transparent: false,
43+
});
44+
45+
const pressedKeys = new Set<number>();
46+
let mouseLatch = false;
47+
let lastProfile = "runs 0 / best 0";
48+
let lastAlert = "Stand by.";
49+
50+
window.on("keyDown", (event) => {
51+
const data = event as { data?: { keyCode?: number } };
52+
if (typeof data.data?.keyCode === "number") {
53+
pressedKeys.add(data.data.keyCode);
54+
}
55+
});
56+
window.on("keyUp", (event) => {
57+
const data = event as { data?: { keyCode?: number } };
58+
if (typeof data.data?.keyCode === "number") {
59+
pressedKeys.delete(data.data.keyCode);
60+
}
61+
});
62+
63+
const scene = createDefenseSceneGpu(window, FALLBACK_THEME.scene);
64+
65+
const stop = await startBattlestationRuntime({
66+
mission: FALLBACK_MISSION,
67+
theme: FALLBACK_THEME,
68+
scene,
69+
audio: createSilentAudioAdapter(),
70+
controls: {
71+
getSelectionAxis() {
72+
if ([...LEFT_KEYS].some((key) => pressedKeys.has(key))) return -1;
73+
if ([...RIGHT_KEYS].some((key) => pressedKeys.has(key))) return 1;
74+
return 0;
75+
},
76+
getFirePressed() {
77+
return [...FIRE_KEYS].some((key) => pressedKeys.has(key));
78+
},
79+
getMutePressed() {
80+
return [...MUTE_KEYS].some((key) => pressedKeys.has(key));
81+
},
82+
drainPointerTargets() {
83+
const buttons = Screen.getMouseButtons();
84+
const leftDown = (buttons & 1n) === 1n;
85+
const targets: Array<{ x: number; y: number }> = [];
86+
87+
if (leftDown && !mouseLatch) {
88+
const cursor = Screen.getCursorScreenPoint();
89+
const frame = window.getFrame();
90+
// Electrobun currently exposes the outer window frame here, not an
91+
// explicit content-rect API. That means native chrome can slightly
92+
// skew top-edge click mapping until a content-area coordinate surface
93+
// is available on the Bun side.
94+
const inside = cursor.x >= frame.x
95+
&& cursor.x <= frame.x + frame.width
96+
&& cursor.y >= frame.y
97+
&& cursor.y <= frame.y + frame.height;
98+
if (inside && frame.width > 0 && frame.height > 0) {
99+
targets.push({
100+
x: ((cursor.x - frame.x) / frame.width) * 640,
101+
y: ((cursor.y - frame.y) / frame.height) * 640,
102+
});
103+
}
104+
}
105+
106+
mouseLatch = leftDown;
107+
return targets;
108+
},
109+
dispose() {},
110+
},
111+
hud: {
112+
updateMission(view) {
113+
const target = view.selected_contact_id === null ? "NONE" : `#${view.selected_contact_id}`;
114+
window.setTitle(
115+
`A130 Defense | ${view.mission_state} | score ${view.score} | integrity ${view.integrity} | target ${target}`,
116+
);
117+
},
118+
updateProfile(profile) {
119+
lastProfile = `runs ${profile.missionsRun} / best ${profile.bestScore}`;
120+
},
121+
setAlert(text) {
122+
lastAlert = text;
123+
console.log(text);
124+
},
125+
setAlertLog(lines) {
126+
if (lines.length > 0) {
127+
console.log(lines[0]);
128+
}
129+
},
130+
setStatus(text) {
131+
window.setTitle(`${text} | ${lastProfile} | ${lastAlert}`);
132+
},
133+
},
134+
});
135+
136+
window.on("close", () => {
137+
stop();
138+
});
139+
}
140+
141+
main().catch(console.error);
Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1 @@
1-
import { BrowserWindow } from "electrobun/bun";
2-
3-
const isProduction = process.env.NODE_ENV === "production";
4-
const url = isProduction ? "views://main/index.html" : "http://localhost:1420";
5-
6-
new BrowserWindow({
7-
title: "gametau battlestation (electrobun showcase)",
8-
url,
9-
});
1+
import "./browser";
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export interface MissionStubConfig {
2+
callsign: string;
3+
sector: string;
4+
objective: string;
5+
intel: string;
6+
tacticalProtocol: string[];
7+
}
8+
9+
export interface ThemeConfig {
10+
scene: SceneTheme;
11+
audio: {
12+
hitHz: number;
13+
killConfirmHz: number;
14+
missHz: number;
15+
integrityLossHz: number;
16+
criticalAlertHz: number;
17+
};
18+
ui: {
19+
criticalIntegrityThreshold: number;
20+
};
21+
}
22+
23+
export interface SceneTheme {
24+
background: string;
25+
grid: string;
26+
selected: string;
27+
shipColor: string;
28+
friendlyColor: string;
29+
contactPulseSpeed?: number;
30+
contactPulseAmount?: number;
31+
}
32+
33+
export const FALLBACK_MISSION: MissionStubConfig = {
34+
callsign: "LANCE-130",
35+
sector: "Outer Grid Delta-7",
36+
objective: "Defend friendly cluster from approaching hostiles.",
37+
intel: "No mission config found. Using fallback profile.",
38+
tacticalProtocol: [],
39+
};
40+
41+
export const FALLBACK_THEME: ThemeConfig = {
42+
scene: {
43+
background: "#030608",
44+
grid: "#1a3040",
45+
selected: "#ffe680",
46+
shipColor: "#00e5ff",
47+
friendlyColor: "#22cc44",
48+
},
49+
audio: {
50+
hitHz: 660,
51+
killConfirmHz: 880,
52+
missHz: 220,
53+
integrityLossHz: 160,
54+
criticalAlertHz: 120,
55+
},
56+
ui: {
57+
criticalIntegrityThreshold: 25,
58+
},
59+
};

0 commit comments

Comments
 (0)