fuzzyworld

explodorama / files / src / ui / padTarget.ts

padTarget.ts

Blame: the commit that last changed each line · file · history

720cc9d3 2026-11-03 Pavel Safronov1import { cellAt, eq } from '../sim/state'
ebcd0a8f 2026-11-02 Pavel Safronov2import type { GameState } from '../sim/types'
3import { tileInfo } from './info'
4import { nextFocus, type Step } from './spatial'
5
6/**
7 * The controller's current target among the tiles a picked action can use (the blue ones). Pure:
8 * main.ts hands in the tiles and the player's tile.
9 */
10interface Tile {
11 x: number
12 y: number
13}
14
15/** The tile nearest the player (straight-line distance; a tie goes to the smaller y, then x), or null for none. */
16export function firstTarget(tiles: readonly Tile[], player: Tile): Tile | null {
17 let best: Tile | null = null
18 let bestKey: [number, number, number] = [Infinity, 0, 0]
19 for (const t of tiles) {
20 const key: [number, number, number] = [Math.hypot(t.x - player.x, t.y - player.y), t.y, t.x]
21 if (key[0] < bestKey[0] || (key[0] === bestKey[0] && (key[1] < bestKey[1] || (key[1] === bestKey[1] && key[2] < bestKey[2])))) {
22 best = t
23 bestKey = key
24 }
25 }
26 return best
27}
28
29/**
30 * The target a direction jumps to from `current`: the best tile that way (spatial.ts `nextFocus`,
31 * board coordinates, y growing down as the screen's does), or `current` if none lies that way.
32 * Nothing wraps round.
33 */
34export function jumpTarget(tiles: readonly Tile[], current: Tile, dx: Step, dy: Step): Tile {
35 const at = tiles.findIndex((t) => t.x === current.x && t.y === current.y)
36 if (at < 0) return firstTarget(tiles, current) ?? current
37 const rects = tiles.map((t) => ({ x: t.x, y: t.y, w: 1, h: 1 }))
38 return tiles[nextFocus(rects, at, dx, dy)] ?? current
39}
40
720cc9d3 2026-11-03 Pavel Safronov41/** A pillar or outer wall that is nothing else (not cracked, not about to fall). */
42export function isPlainWall(s: GameState, p: Tile): boolean {
43 const i = p.y * s.width + p.x
44 return cellAt(s, p) === 'wall' && !s.cracked.includes(i) && !s.collapsing.some((c) => c.cells.includes(i))
45}
46
47/** Where the aim starts: the nearest tile with something to say besides a plain wall (those are all around), else the nearest of all. */
48export function aimStart(s: GameState, tiles: readonly Tile[]): Tile | null {
49 const others = tiles.filter((t) => !eq(t, s.player))
50 return firstTarget(others.filter((t) => !isPlainWall(s, t)), s.player) ?? firstTarget(others, s.player) ?? null
51}
52
ebcd0a8f 2026-11-02 Pavel Safronov53/**
54 * Every tile "What is that?" has something to say about, for the controller's inspect aim: what
55 * `tileInfo` explains (enemies, bombs, crates, metal, rubble, barrels, hazards, bushes, oil, gems,
56 * the exit, drops, a decoy, flame, a falling ceiling, a cracked pillar), nearest the player first.
57 * Plain pillars and the outer wall are left out: there are dozens, and the nearest would always be
720cc9d3 2026-11-03 Pavel Safronov58 * one of them (a tap or click still explains them). The player's own tile is left out too, unless `plain` is on (Q86).
59 * `plain`: those are in too (Q82: the aim must be able to ask about a pillar or a wall).
ebcd0a8f 2026-11-02 Pavel Safronov60 */
720cc9d3 2026-11-03 Pavel Safronov61export function explainableTiles(s: GameState, plain = false): Tile[] {
ebcd0a8f 2026-11-02 Pavel Safronov62 const tiles: Tile[] = []
63 for (let y = 0; y < s.height; y++) {
64 for (let x = 0; x < s.width; x++) {
65 const p = { x, y }
720cc9d3 2026-11-03 Pavel Safronov66 if (!plain && isPlainWall(s, p)) continue
67 // Yourself is only in the aim's own list (`plain`; the demo's keeps the old one).
68 if (!plain && eq(p, s.player)) continue
ebcd0a8f 2026-11-02 Pavel Safronov69 if (tileInfo(s, p)) tiles.push(p)
70 }
71 }
72 return tiles.sort((a, b) => Math.hypot(a.x - s.player.x, a.y - s.player.y) - Math.hypot(b.x - s.player.x, b.y - s.player.y) || a.y - b.y || a.x - b.x)
73}