SOURCE / PINNED RELEASE
Made of little things.
Black Site
- Release
- 666bc7c74ebd…
- Author-recorded commit
- 05e95af79676…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-d2225ddc391
Archive hash verified: 1544e072e245c4a8…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { KEYS_DOMAIN, keys, type Action, type Subscription } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
const actions: (Action & { code: string })[] = [
{ id: 'blacksite.primary', label: 'Equip primary weapon', defaultKey: '1', code: 'Digit1' },
{ id: 'blacksite.secondary', label: 'Equip secondary weapon', defaultKey: '2', code: 'Digit2' },
{ id: 'blacksite.throwable', label: 'Equip throwable', defaultKey: '3', code: 'Digit3' },
{ id: 'blacksite.medical', label: 'Equip medical item', defaultKey: '4', code: 'Digit4' },
{ id: 'blacksite.reload', label: 'Reload', defaultKey: 'r', code: 'KeyR' },
{ id: 'blacksite.knife', label: 'Knife attack', defaultKey: 'q', code: 'KeyQ' },
{ id: 'blacksite.interact', label: 'Interact', defaultKey: 'f', code: 'KeyF' },
{ id: 'blacksite.inventory', label: 'Toggle inventory', defaultKey: 'i', code: 'KeyI' },
{ id: 'blacksite.pause', label: 'Pause or resume', defaultKey: 'p', code: 'KeyP' },
];
function editing(event: KeyboardEvent): boolean {
return event.isComposing || (event.target instanceof Element &&
Boolean(event.target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])')));
}
/** Runtime owns forwarding and remapping; this adapter owns gameplay state. */
export function installGameKeyboard(
held: (code: string, down: boolean) => void,
invoke: (code: string) => void,
reset: () => void,
): () => void {
const registered = new Set<string>();
const subscriptions = new Map<string, Subscription>();
const attempted = new Set<string>();
let closed = false;
let currentEvent: KeyboardEvent | undefined;
// Runs before the runtime's document capture listener. Its onAction callback
// has no event argument, so retain this event until the next task (browsers run microtasks between event listeners).
const capture = (event: KeyboardEvent) => {
currentEvent = event;
window.setTimeout(() => { if (currentEvent === event) currentEvent = undefined; }, 0);
};
const down = (event: KeyboardEvent) => {
if (event.defaultPrevented || editing(event) || event.ctrlKey || event.altKey || event.metaKey) return;
held(event.code, true);
if (event.repeat) return;
if (event.code === 'Escape' || actions.some(action => action.code === event.code && !registered.has(action.id))) {
invoke(event.code);
}
};
const up = (event: KeyboardEvent) => held(event.code, false);
const hidden = () => { if (document.hidden) reset(); };
window.addEventListener('keydown', capture, true);
window.addEventListener('keydown', down);
window.addEventListener('keyup', up);
window.addEventListener('blur', reset);
document.addEventListener('visibilitychange', hidden);
const release = (id: string) => {
subscriptions.get(id)?.close();
subscriptions.delete(id);
registered.delete(id);
try { keys.unregisterAction(id); } catch { /* Runtime may already be gone. */ }
};
if (runtimeHasDomain(KEYS_DOMAIN)) {
for (const { code, ...action } of actions) {
void (async () => {
try {
// Subscribe before registration so an immediate binding push is usable.
// Some runtimes capture on window before our listener; defer until
// all capture listeners have seen the current event.
subscriptions.set(action.id, keys.onAction(action.id, () => queueMicrotask(() => {
if (closed || (currentEvent && (currentEvent.repeat || editing(currentEvent)))) return;
if (currentEvent) held(currentEvent.code, false); // Clear only a consumed rebound key.
invoke(code);
})));
attempted.add(action.id);
await keys.registerAction(action);
if (closed) release(action.id);
else registered.add(action.id);
} catch (error) {
release(action.id);
console.warn(`Keyboard action ${action.id} unavailable; using local shortcut.`, error);
}
})();
}
}
const close = () => {
if (closed) return;
closed = true;
window.removeEventListener('keydown', capture, true);
window.removeEventListener('keydown', down);
window.removeEventListener('keyup', up);
window.removeEventListener('blur', reset);
document.removeEventListener('visibilitychange', hidden);
window.removeEventListener('pagehide', close);
for (const id of attempted) release(id);
reset();
};
window.addEventListener('pagehide', close);
return close;
}
