explodorama / files / src / ui / targetLabel.ts
targetLabel.ts
ebcd0a8f 2026-11-02 Pavel Safronov | 1 | import { isDeadly } from '../sim/danger' |
| 2 | import { pickChoices, resolvePick } from '../sim/picks' | |
| 3 | import { enemyAt } from '../sim/enemies' | |
| 4 | import { bombAt, oilSlide, playerCanPass } from '../sim/state' | |
| 5 | import type { ActionId, GameState, Vec } from '../sim/types' | |
| 6 | import { CHARGE_TEXT } from './info' | |
| 7 | ||
| 8 | /** | |
| 9 | * The controller's one-line label (user request, 2026-11-01): what confirming the current target | |
| 10 | * will do, in the player's words. `picked` is the action button (Move, Bomb, Charge, Wait); the | |
| 11 | * tile decides which of its actions it is (picks.ts `resolvePick`), as it does for a tap. `target` | |
| 12 | * is null for Wait, which has no tile. An action that would cost a life this turn says so. | |
| 13 | */ | |
| 14 | export function targetLabel(s: GameState, picked: ActionId, target: Vec | null): string { | |
| 15 | if (!target) return picked === 'wait' ? withDanger('Wait a turn', isDeadly(s, { id: 'wait' })) : 'Nothing to do here' | |
| 16 | // A bomb two of the button's actions could deal with: the choice dialog asks which. | |
| 17 | if (pickChoices(s, picked, target).length) return 'Choose what to do with the bomb on this tile' | |
| 18 | const id = resolvePick(s, picked, target) | |
| 19 | if (!id) return 'Nothing to do here' | |
| 20 | return withDanger(labelOf(s, id, target), isDeadly(s, { id, target })) | |
| 21 | } | |
| 22 | ||
| 23 | function withDanger(text: string, deadly: boolean): string { | |
| 24 | return deadly ? `${text} (costs a life!)` : text | |
| 25 | } | |
| 26 | ||
| 27 | /** A step or run onto oil goes on sliding along the spill (turn.ts), so it is more than a move there. */ | |
| 28 | function slidesOnOil(s: GameState, target: Vec): boolean { | |
| 29 | const d = { x: Math.sign(target.x - s.player.x), y: Math.sign(target.y - s.player.y) } | |
| 30 | return oilSlide(s, target, d, (q) => playerCanPass(s, q) && !enemyAt(s, q)).length > 0 | |
| 31 | } | |
| 32 | ||
| 33 | function labelOf(s: GameState, id: ActionId, target: Vec): string { | |
| 34 | switch (id) { | |
| 35 | case 'move': return slidesOnOil(s, target) ? 'Slide to the end of the oil' : bombAt(s, target) ? 'Walk onto the bomb on this tile' : 'Move to this tile' | |
| 36 | case 'run': return slidesOnOil(s, target) ? 'Slide to the end of the oil' : 'Run to this tile' | |
| 37 | case 'diagonal': return 'Step diagonally to this tile' | |
| 38 | case 'slide': return 'Slide to this tile' | |
| 39 | case 'kick': return 'Kick the bomb on this tile' | |
| 40 | case 'bomb': return 'Place a bomb on this tile' | |
| 41 | case 'throw': return 'Throw a bomb to this tile' | |
| 42 | case 'rpg': return 'Fire an RPG bomb to this tile' | |
| 43 | case 'detonate': return 'Set off the bomb on this tile' | |
| 44 | case 'wait': return 'Wait a turn' | |
| 45 | case 'icbm': return 'Launch an ICBM to this tile' | |
| 46 | case 'freeze': return 'Freeze every enemy' | |
| 47 | case 'decoy': return 'Put a decoy on this tile' | |
| 48 | case 'teleport': return 'Teleport to this tile' | |
| 49 | case 'charged': return `Place a ${s.charge ? CHARGE_TEXT[s.charge.kind].name : 'charged'} bomb on this tile` | |
| 50 | } | |
| 51 | } |