explodorama / files / src / ui / padGlyph.ts
padGlyph.ts
| 1 | import { faceOf, type FaceIndex, type PadFamily } from './padMap' |
| 2 | |
| 3 | /** |
| 4 | * Which controller picture goes on an action-bar button (controller support; Kenney Input |
| 5 | * Prompts, assets/kenney-input-prompts). The buttons are the board mapping of the face buttons |
| 6 | * (padMap.ts): Move/Go on Cross or A, Bomb on Square or X, Charge on Triangle or Y, Wait on Circle |
| 7 | * or B, by position on the pad (bottom, left, top, right). A Nintendo pad is the exception: its A |
| 8 | * (right) is Move/Go and its B (bottom) is Wait, Bomb Y and Charge X as before. A pad that is none |
| 9 | * of the three has the same layout (W3C standard mapping), so it gets the Xbox letters. |
| 10 | */ |
| 11 | export type GlyphButton = 'move' | 'bomb' | 'charge' | 'wait' |
| 12 | |
| 13 | /** The picture of the face button at each raw index (0 bottom, 1 right, 2 left, 3 top), by family. */ |
| 14 | const FACE_NAMES = { |
| 15 | playstation: ['cross', 'circle', 'square', 'triangle'], |
| 16 | xbox: ['a', 'b', 'x', 'y'], |
| 17 | // Nintendo's letters sit in other places: bottom B, right A, left Y, top X. |
| 18 | nintendo: ['b', 'a', 'y', 'x'], |
| 19 | } as const |
| 20 | |
| 21 | /** The picture for a raw face button (0 bottom .. 3 top), as dialogs use them. */ |
| 22 | export function faceGlyph(family: PadFamily, face: FaceIndex): string { |
| 23 | if (family === 'playstation') return `playstation_button_color_${FACE_NAMES.playstation[face]}` |
| 24 | if (family === 'nintendo') return `switch_button_${FACE_NAMES.nintendo[face]}` |
| 25 | return `xbox_button_color_${FACE_NAMES.xbox[face]}` |
| 26 | } |
| 27 | |
| 28 | /** The sprite's name (the file without `.png`) for `button` on a pad of `family`: the picture of the face button that does it there. */ |
| 29 | export function glyphFor(family: PadFamily, button: GlyphButton): string { |
| 30 | return faceGlyph(family, faceOf(family, button)) |
| 31 | } |
| 32 | |
| 33 | /** The shoulder buttons' pictures (the board's hint): L1 / LB / L and R1 / RB / R. A pad that is none of the three gets the Xbox ones. */ |
| 34 | export function shoulderGlyph(family: PadFamily, side: 'l' | 'r'): string { |
| 35 | if (family === 'playstation') return `playstation_trigger_${side}1` |
| 36 | if (family === 'nintendo') return `switch_button_${side}` |
| 37 | return `xbox_${side}b` |
| 38 | } |
| 39 | |
| 40 | /** The d-pad and right-stick pictures, for the demo's virtual controller (padOverlay.ts). */ |
| 41 | export type DpadDir = 'none' | 'up' | 'down' | 'left' | 'right' |
| 42 | export type StickDir = 'none' | 'left' | 'right' | 'up' | 'down' | 'press' |
| 43 | |
| 44 | /** The file-name prefix of a family's d-pad and stick pictures; a pad that is none of the three gets the Xbox ones. */ |
| 45 | function prefix(family: PadFamily): string { |
| 46 | return family === 'playstation' ? 'playstation' : family === 'nintendo' ? 'switch' : 'xbox' |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * The d-pad with `dir` lit (`none`: the plain, all-white d-pad). The plain one and the lit ones are |
| 51 | * all the filled style, so pressing only turns one arm red and the picture does not change shape |
| 52 | * (Pavel, 2026-11-02; Kenney's `_dpad_none` is an outline, which looked like a different picture). |
| 53 | */ |
| 54 | export function dpadGlyph(family: PadFamily, dir: DpadDir): string { |
| 55 | return dir === 'none' ? `${prefix(family)}_dpad` : `${prefix(family)}_dpad_${dir}` |
| 56 | } |
| 57 | |
| 58 | /** The right stick pushed towards `dir`, or pressed in (`none`: at rest). */ |
| 59 | export function stickGlyph(family: PadFamily, dir: StickDir): string { |
| 60 | return dir === 'none' ? `${prefix(family)}_stick_r` : `${prefix(family)}_stick_r_${dir}` |
| 61 | } |
| 62 | |
| 63 | /** The Start button's picture: Options / Menu / Plus. */ |
| 64 | export function startGlyph(family: PadFamily): string { |
| 65 | return family === 'playstation' ? 'playstation5_button_options' : family === 'nintendo' ? 'switch_button_plus' : 'xbox_button_menu' |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * A demo caption with its `{token}`s turned into the controller's pictures, as inline `<img |
| 70 | * class="pad-glyph">` (`url` turns a sprite name into its address: `spriteUrl`). Tokens: {dpad}, |
| 71 | * {move}, {bomb}, {charge}, {wait}, {l1}, {r1}, {stick}, {start}. An unknown token throws, so a |
| 72 | * typo in a caption shows up in the tests. The text itself is not escaped: captions are ours. |
| 73 | */ |
| 74 | export function padCaptionHtml(text: string, family: PadFamily, url: (name: string) => string): string { |
| 75 | return text.replace(/\{(\w+)\}/g, (_, token: string) => { |
| 76 | let name: string |
| 77 | switch (token) { |
| 78 | case 'dpad': name = dpadGlyph(family, 'none'); break |
| 79 | case 'move': case 'bomb': case 'charge': case 'wait': name = glyphFor(family, token); break |
| 80 | case 'l1': name = shoulderGlyph(family, 'l'); break |
| 81 | case 'r1': name = shoulderGlyph(family, 'r'); break |
| 82 | case 'stick': name = stickGlyph(family, 'none'); break |
| 83 | case 'start': name = startGlyph(family); break |
| 84 | default: throw new Error(`Unknown caption token {${token}}`) |
| 85 | } |
| 86 | return `<img class="pad-glyph" src="${url(name)}" alt="">` |
| 87 | }) |
| 88 | } |