|
| 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); |
0 commit comments