From 461636168c797a321556751cff5c228edc150cc5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:51:37 +0000 Subject: [PATCH] Blockilization: an aircraft pointed at an enemy out of range says so A Bomber (any aircraft) clicked onto an enemy it couldn't strike, further off than its range or out of sight, was sent to rebase there instead, and the refusal read "Only to one of your cities (six aircraft) or Carriers (three) with room". The tile's card said the same, and the unit card's line said "Out of range (10): move to fire", though an aircraft can't move. Now the click, the tile's card and the unit card all say why it can't strike ("Out of range: it strikes up to 10 tiles away", or "Not in sight"), and the aircraft keeps its move. Over one of its bases the unit card says it rebases there (it said one soldier to a tile when the city had a garrison), and the unit card's help says it strikes only what you can see. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01U4mVqwqfnbw2LyX5eF58Fa --- src/games/blockilization/client/input.ts | 11 +++++++---- src/games/blockilization/client/path.ts | 4 +++- src/games/blockilization/client/store.ts | 14 ++++++++++++++ src/games/blockilization/client/ui.ts | 13 ++++++++++--- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/games/blockilization/client/input.ts b/src/games/blockilization/client/input.ts index f89916b..e7d8544 100644 --- a/src/games/blockilization/client/input.ts +++ b/src/games/blockilization/client/input.ts @@ -4,11 +4,11 @@ import type { CityWire, PreviewAsk, UnitWire } from '../protocol'; import { cam, centreHere, pick, zoom } from './camera'; import { flagsIn, hitAt, type Hit } from './overlay'; import { ask, attackable, cityAt, enemyAt, isNuke, me, myCity, myUnit, store, unitsAt } from './store'; -import { closePanels, endTurnAction, explores, front, openCity, platformMenuOpen, selectNextIdle, send, setTipPosition, toggleLens, togglePanel } from './ui'; +import { closePanels, endTurnAction, explores, front, openCity, platformMenuOpen, selectNextIdle, send, setTipPosition, toast, toggleLens, togglePanel } from './ui'; import { openPedia, tileArticle } from './pedia'; import { openOverview } from './overview'; import { cycleLens } from './lenses'; -import { chargeFrom } from './path'; +import { chargeFrom, whyNot } from './path'; import { marchTo } from './march'; import { play } from './sound'; import { closePinMenu, openPinMenu } from './pins'; @@ -205,8 +205,11 @@ function act(t: number, right: boolean, hit: Hit | null = null, touch = false, s const unit = sel.id; ask(`Launch the ${UNITS[sel.type].name} here? Everything in the blast is struck, its land left in fallout, and every civ that knows you will long remember it. It can only fall where everyone it would hit is someone you're at war with.`, 'Launch it', () => send({ t: 'attack', unit, target: t })); } else send({ t: 'attack', unit: sel.id, target: t }); - } else if (UNITS[sel.type]?.domain === 'air') send({ t: 'rebase', unit: sel.id, to: t }); - else { + } else if (UNITS[sel.type]?.domain === 'air') { + // (At an enemy it can't strike, out of range or sight: why not, rather than a rebase there that can't be.) + if (enemyAt(t)) toast(whyNot(sel, t)); + else send({ t: 'rebase', unit: sel.id, to: t }); + } else { // A melee unit sent at an enemy it can reach this turn charges: next to it, then the attack. const from = chargeFrom(sel, t); if (from !== null) { diff --git a/src/games/blockilization/client/path.ts b/src/games/blockilization/client/path.ts index 87b63c5..c9dd1e5 100644 --- a/src/games/blockilization/client/path.ts +++ b/src/games/blockilization/client/path.ts @@ -4,7 +4,7 @@ import { dangerCost, dangerZones, type Danger } from '../rules/danger'; import { ALL, findWay, neighbours, type Walker } from '../rules/hex'; import { FEATURES, HILLS, RAIL_MOVE, ROAD_MOVE, TERRAINS } from '../rules/terrain'; import { heedsBorders, isGreatPerson, isMilitary, stackKind, UNITS } from '../rules/units'; -import { cityAt, enemyAt, me, outOfReach, store } from './store'; +import { airWhyNot, cityAt, enemyAt, me, outOfReach, store } from './store'; /** * The way a unit would go, as this screen can tell (the server works out the real one when it's @@ -261,6 +261,8 @@ export function whyNot(u: UnitWire, to: number): string { const view = store.view; const type = UNITS[u.type]; if (!view || !type) return "Can't get there"; + // (An aircraft doesn't go by a way: it strikes, or rebases.) + if (type.domain === 'air') return airWhyNot(u, to); const military = soldier(u); const there = ownThere(u, to); if (there) return `Your ${unitName(there)} is there (one ${military ? 'soldier' : 'civilian'} to a tile)`; diff --git a/src/games/blockilization/client/store.ts b/src/games/blockilization/client/store.ts index 428cd9c..c191b90 100644 --- a/src/games/blockilization/client/store.ts +++ b/src/games/blockilization/client/store.ts @@ -346,6 +346,20 @@ export function outOfReach(u: UnitWire, t: number): string { return g && distance(g, u.tile, t) <= reachOf(u) ? 'Hills, forest, jungle or mountains block its shot' : `Out of range (${reachOf(u)})`; } +/** + * Why an aircraft can't strike a tile or rebase to it: an enemy there out of its range, or out of + * sight (it strikes only what its side sees, as the rules' `airTarget` has it); else no base of + * yours there in reach. + */ +export function airWhyNot(u: UnitWire, t: number): string { + const g = store.grid; + if (g && enemyAt(t)) { + if (distance(g, u.tile, t) > reachOf(u)) return `Out of range: it strikes up to ${reachOf(u)} tiles away`; + if (!store.known[t]?.visible) return 'Not in sight: it strikes only what you can see now'; + } + return `It rebases only to your cities and ships with room for it, within ${reachOf(u) * 2} tiles`; +} + /** * Whether a unit could attack a tile from where it stands: a fighting unit, an enemy in sight there, * in reach; and for a shot past the next tile, a line of sight to it, as the rules' `canFire` has it diff --git a/src/games/blockilization/client/ui.ts b/src/games/blockilization/client/ui.ts index c000220..d2b51a7 100644 --- a/src/games/blockilization/client/ui.ts +++ b/src/games/blockilization/client/ui.ts @@ -143,6 +143,11 @@ export const send = (o: Order) => { }; /** An order the screen gives for the player (a march's, turn by turn). */ export const sendFor = (o: Order) => client.send('order', o); +/** A word at the top of the screen for a moment, as an order refused gets. */ +export function toast(text: string) { + store.toast = { text, until: client.time + 3 }; + store.changed++; +} /** Ask the server what another civ would make of a deal being drawn up (its answer comes as `dealCheck`). */ export const checkDeal = (data: { target: number; items: DealItem[]; key: string }) => client.send('dealCheck', data); const lobby = (a: LobbyAction) => client.send('lobby', a); @@ -1431,7 +1436,7 @@ function renderUnit(): El | null { : type.abilities?.includes('combat_bonus_aura') ? `${ABILITY_TEXT.combat_bonus_aura} Keep it with your army (it can share a tile with a soldier), out of the enemy's reach.` : type.domain === 'air' - ? `${click(true)} an enemy within ${reachOf(u)} tiles to strike it (they're ringed in red), or one of your cities or ships outlined in blue to rebase there. Fighters and anti-aircraft guns near a target may intercept.${type.class === 'missile' ? ' The missile is spent in its strike.' : ''}` + ? `${click(true)} an enemy you can see within ${reachOf(u)} tiles to strike it (they're ringed in red), or one of your cities or ships outlined in blue to rebase there. Fighters and anti-aircraft guns near a target may intercept.${type.class === 'missile' ? ' The missile is spent in its strike.' : ''}` : type.ranged ? `Enemies in reach are ringed in red: ${touchScreen() ? 'tap one to see how a shot would go, tap it again to fire' : 'point at one to see how a shot would go, click to fire'}.` : type.strength @@ -1569,7 +1574,9 @@ function renderArrival() { let cls = ''; if (u && t >= 0 && t !== u.tile && store.known[t]) { const route = attackable(u, t) ? undefined : previewPath(u, t); - if (route === null) [text, cls] = [whyNot(u, t), 'no']; + // (An aircraft goes by no way: it rebases there, or it's told why not.) + if (route === null && u.bases?.includes(t)) text = 'Rebases here: that takes its turn'; + else if (route === null) [text, cls] = [whyNot(u, t), 'no']; else if (route) text = [wayThere(route, u), wallsOnWay(route, u)].filter(Boolean).join(' ยท '); } if (arrival.textContent !== text) { @@ -2629,7 +2636,7 @@ function renderTip() { // (An aircraft rebases, rather than going by a way: where it could, or why not.) if (sel && t !== sel.tile && !attackable(sel, t) && UNITS[sel.type]?.domain === 'air') { const base = sel.bases?.includes(t); - bits.push(h(`div.ln.way${base ? '' : '.no'}`, {}, icon(base ? 'plane' : 'close', 13), base ? `${click(true)} to rebase it here` : `It rebases only to your cities and ships with room for it, within ${reachOf(sel) * 2} tiles`)); + bits.push(h(`div.ln.way${base ? '' : '.no'}`, {}, icon(base ? 'plane' : 'close', 13), base ? `${click(true)} to rebase it here` : whyNot(sel, t))); } else if (sel && t !== sel.tile && !attackable(sel, t)) { const route = previewPath(sel, t); bits.push(h(`div.ln.way${route ? '' : '.no'}`, {}, icon(route ? 'moves' : 'close', 13), route ? wayThere(route, sel) : whyNot(sel, t)));