import { COLORS } from '../../config.js'; import { cityById, cityAt, tileIndex, defenderStrength, unitsOnBoat, IMP } from './CivilizationLogic.js'; // Flavor/ability text for unit flags — data/civilization-rules.json has no // description field on units, so this is authored here. A few flags // (aegis, paradrop, amphibious, fighter) aren't mechanically wired in // CivilizationLogic.js/CivilizationAI.js yet; their text is standard // Civilization-series flavor phrasing, harmless either way. const FLAG_TEXT = { settler: 'Can found new cities and build terrain improvements (irrigation, roads, mines).', coastal: 'Coast-hugging vessel — cannot cross open ocean away from land.', mounted: 'Fast cavalry — takes double damage from anti-mounted defenders.', paradrop: 'Can drop directly onto a target tile within range, bypassing the terrain in between.', ignoreterrain: 'Ignores terrain movement penalties — always costs 1 movement point per tile.', antimounted: 'Doubles its defense strength against mounted attackers.', aegis: 'Advanced systems grant bonus defense against air and missile attacks.', spaceship: 'A spaceship component — built toward the space-race victory instead of combat.', amphibious: 'No attack penalty when assaulting land units directly from a ship.', engineer: 'Works twice as fast on terrain improvements and can perform advanced terraforming.', missile: 'One-shot weapon — consumed whether or not the attack succeeds.', nuke: 'Devastating strike that levels the target; can be intercepted by SDI Defense.', fighter: 'Limited range — must return to a city or carrier to refuel.', submarine: 'Stealthed — invisible to enemies unless one of their units or cities is adjacent.', noncombat: 'Cannot attack or be used to initiate combat.', ignorewalls: 'Attack ignores the defensive bonus from City Walls.', caravan: 'Can be sent to another city to establish a trade route or help rush a Wonder.', }; // Benefit text for building effects — again authored here since the rules // JSON has no description field. Each entry is a function of the building // so magnitude (building.value) can be interpolated where the game engine // actually uses that value (see CivilizationLogic.js); a couple of effects // (sizecap, palace, granary, veterans, airport, sdi) are described by what // the engine actually does rather than by the value field, since that field // isn't consumed literally for them. const EFFECT_TEXT = { roadtrade: (b) => `Boosts trade from roaded tiles by ${Math.round(b.value * 100)}%.`, science: (b) => `+${Math.round(b.value * 100)}% science output from this city.`, airport: () => 'Air units based here can rebase instantly and heal fully each turn.', granary: () => 'Keeps half the food box on hand when the city grows, speeding regrowth.', walls: (b) => `Multiplies this city's defense against land attacks by ${b.value} (unless the attacker ignores walls).`, sdi: () => 'Automatically intercepts incoming nuclear missiles aimed at this city.', farmfood: (b) => `+${Math.round(b.value * 100)}% food from farmland-improved tiles worked by this city.`, defenseair: (b) => `Multiplies this city's defense against air attacks by ${b.value}.`, power: (b) => `+${Math.round(b.value * 100)}% shield output from the city's Factory (requires a Factory).`, corruption: (b) => `Cuts this city's corruption and waste by ${Math.round(b.value * 100)}%.`, gold: (b) => `+${Math.round(b.value * 100)}% gold income from this city's trade.`, veterans: () => 'Units built here start as veterans, with a combat bonus.', sizecap: (b) => (b.id === 'sewersystem' ? 'Removes this city\'s population growth cap entirely.' : 'Raises this city\'s population cap so it can keep growing.'), oceanfood: (b) => `+${b.value} food from every ocean tile this city works.`, shields: (b) => `+${Math.round(b.value * 100)}% shield (production) output from this city.`, oceanshield: (b) => `+${b.value} shield from every ocean tile this city works.`, palace: () => 'Makes this your capital — corruption and waste grow with distance from it.', defensesea: (b) => `Multiplies this city's defense against naval attacks by ${b.value}.`, }; // Short blurbs for the handful of tech-unlocked governments — again hand // authored since the rules JSON carries only raw numeric fields, keyed off // what CivilizationLogic.js actually does with those fields (tradeBonus, // corruptionFactor, despotPenalty, freeUnits, unitUpkeep, warPenalty). export const GOVERNMENT_TEXT = { despotism: 'The starting government: heavy corruption and a yield penalty on productive tiles, but cheap unit support.', anarchy: 'Lawless transition between governments — crippling corruption and no research. Ends on its own.', monarchy: 'Cuts corruption well below Despotism and drops the Despotism penalty to tile yields.', communism: 'Low, distance-independent corruption anywhere in the empire, plus 6 free unit upkeep per city.', republic: 'Sharply reduces corruption and adds +1 trade to tiles already producing trade, at the cost of unit upkeep.', democracy: 'Eliminates corruption entirely and adds +1 trade to productive tiles; units cost gold upkeep and broken treaties cost double reputation.', }; // Short blurbs for tech-unlocked terrain improvements (worker orders), keyed // off what CivilizationLogic.js actually does with each one. const IMPROVEMENT_TEXT = { road: 'Cuts movement between roaded tiles to a flat 1 point and adds +1 trade to tiles already producing trade.', railroad: 'Movement between railroaded tiles is free, and adds +1 shield to tiles already producing shields.', irrigation: 'Adds food to the tile, boosting the city that works it.', farmland: 'With a Supermarket in the city, boosts food from irrigated tiles by 50%.', mine: 'Adds shields to the tile, boosting the city that works it.', fortress: 'Doubles a unit\'s defense on the tile and lets even a single unit protect it like a city would.', transform: 'Permanently changes the tile\'s terrain type (Engineers only).', }; export function describeUnitTooltip(rules, unit) { const lines = [ { text: `Attack ${unit.attack} · Defense ${unit.defense} · Move ${unit.move}`, color: COLORS.goldHex }, ]; for (const flag of unit.flags ?? []) { const text = FLAG_TEXT[flag]; if (text) lines.push({ text: `• ${text}` }); } return { title: unit.name, lines }; } export function describeBuildingTooltip(rules, building) { const lines = [ { text: `Cost ${building.cost} shields · Upkeep ${building.upkeep}/turn`, color: COLORS.goldHex }, ]; const describe = EFFECT_TEXT[building.effect]; if (describe) lines.push({ text: `• ${describe(building)}` }); return { title: building.name, lines }; } // Combat is always a duel against the tile's single strongest defender // (pickDefender), so enemy tooltips surface that unit's effective defense — // terrain, fortify, walls etc. included — instead of letting the raw stat // lines suggest the stack defends with combined numbers. `attacker` is the // player's currently-selected unit when there is one (its domain/flags can // change the number, e.g. Pikemen vs mounted); otherwise a plain land unit // stands in. function topDefenderLines(rules, state, units, attacker) { const atk = (attacker && attacker.civ !== units[0].civ) ? attacker : { type: Object.keys(rules.units).find((id) => rules.units[id].domain === 'land' && !rules.units[id].flags.includes('mounted')), }; let top = units[0]; let topD = -1; for (const u of units) { const d = defenderStrength(rules, state, u, atk); if (d > topD) { topD = d; top = u; } } const vs = atk === attacker ? ` vs your ${rules.units[attacker.type].name}` : ''; const lines = [{ text: `Defends at ${topD.toFixed(1)}${vs}`, color: COLORS.goldHex, }]; if (units.length > 1) { lines[0].text = `Top defender: ${rules.units[top.type].name} — defends at ${topD.toFixed(1)}${vs}`; const idx = tileIndex(state.world, top.x, top.y); const protectedStack = !!cityAt(state, top.x, top.y) || !!(state.world.improvements[idx] & IMP.FORTRESS); lines.push({ text: protectedStack ? '• Attacks fight one defender at a time' : '• If the defender falls, the whole stack is lost', }); } return lines; } // Carried units never appear in a tile's unit stack (the map view skips // anything with carriedBy), so summarize a boat's hold here: "2× Musketeers, // 1× Settlers" across every boat in `units`, or null when nothing is aboard. function cargoSummary(rules, state, units) { const counts = new Map(); for (const boat of units) { for (const u of unitsOnBoat(state, boat)) { const name = rules.units[u.type].name; counts.set(name, (counts.get(name) ?? 0) + 1); } } if (!counts.size) return null; return [...counts].map(([name, count]) => `${count}× ${name}`).join(', '); } // Hover tooltip for a map unit/stack: which leader controls it plus a // per-unit-type breakdown. `units` are live game-state unit instances (all // sharing `civ`, since only one civ's units normally occupy a tile), not // rule defs — look each one's def up via rules.units[u.type]. export function describeUnitStackTooltip(rules, state, civ, opponentsData, units, attacker = null) { const opData = opponentsData?.find((o) => o.id === civ.leaderId); const lines = []; if (units.length === 1) { const [u] = units; const def = rules.units[u.type]; lines.push({ text: def.name, color: COLORS.goldHex }); lines.push({ text: `Attack ${def.attack} · Defense ${def.defense} · Move ${def.move}` }); if (civ.human) { const home = cityById(state, u.homeCity); lines.push({ text: `• Home: ${home ? home.name : 'none'}` }); const cargo = cargoSummary(rules, state, [u]); if (cargo) lines.push({ text: `• Carrying: ${cargo}` }); } if (u.vet) lines.push({ text: '• Veteran' }); if (u.fortified) lines.push({ text: '• Fortified' }); for (const flag of def.flags ?? []) { const text = FLAG_TEXT[flag]; if (text) lines.push({ text: `• ${text}` }); } } else { // Friendly stacks group by type + home city so upkeep sources stay // visible; enemy homes are hidden info, so their stacks group by type. const byKey = new Map(); for (const u of units) { const def = rules.units[u.type]; const home = civ.human ? cityById(state, u.homeCity) : null; const key = `${def.name}|${home?.name ?? ''}`; const entry = byKey.get(key) ?? { count: 0, def, home }; entry.count += 1; byKey.set(key, entry); } lines.push({ text: `${units.length} units`, color: COLORS.goldHex }); for (const { count, def, home } of byKey.values()) { const homeTxt = home ? ` · ${home.name}` : ''; lines.push({ text: `${count}× ${def.name} (A${def.attack} D${def.defense} M${def.move})${homeTxt}` }); } if (civ.human) { const cargo = cargoSummary(rules, state, units); if (cargo) lines.push({ text: `Aboard ships: ${cargo}` }); } } if (!civ.human) lines.push(...topDefenderLines(rules, state, units, attacker)); return { title: civ.name, titleColor: civ.color, lines, icon: { texture: 'opponents', frame: opData?.spriteIndex ?? 0, color: civ.color, label: civ.name }, }; } // Hover tooltip for a foreign city: which leader rules it plus the basic // facts visible from outside (population, capital/walls status) — no build // queue or yields, since this is a rival's city and this build has no // espionage/embassy system to justify seeing that. export function describeCityTooltip(rules, civ, opponentsData, city) { const opData = opponentsData?.find((o) => o.id === civ.leaderId); const lines = [ { text: civ.name, color: civ.color }, { text: `Population ${city.size}`, color: COLORS.goldHex }, ]; if (city.buildings.palace) lines.push({ text: '• Capital' }); if (city.buildings.citywalls) lines.push({ text: '• Defended by City Walls' }); return { title: city.name, lines, icon: { texture: 'opponents', frame: opData?.spriteIndex ?? 0, color: civ.color, label: civ.name }, }; } // Hover tooltip for a tech-tree entry: prerequisites (only worth showing // once — omitted for techs the civ already knows) followed by everything // the tech unlocks, grouped by kind. `civ` is used only to decide whether // the tech is already known/repeated, not to filter what's shown. export function describeTechTooltip(rules, tech, civ) { const known = !!civ.known[tech.id] || (tech.repeatable && civ.futureCount > 0); const g = rules.techGates[tech.id]; const lines = []; if (!known && tech.prereqs.length) { lines.push({ text: `Requires: ${tech.prereqs.map((p) => rules.techs[p].name).join(' + ')}`, color: COLORS.mutedHex, }); } const section = (label, items) => { if (!items.length) return; lines.push({ text: label, color: COLORS.goldHex }); for (const item of items) lines.push({ text: `• ${item}` }); }; section('Units:', g.units.map((id) => { const u = rules.units[id]; return `${u.name} (A${u.attack} D${u.defense} M${u.move})`; })); section('Buildings:', g.buildings.map((id) => { const b = rules.buildings[id]; const describe = EFFECT_TEXT[b.effect]; return describe ? `${b.name} — ${describe(b)}` : b.name; })); section('Government:', g.governments.map((id) => { const gv = rules.governments[id]; const text = GOVERNMENT_TEXT[id]; return text ? `${gv.name} — ${text}` : gv.name; })); section('Terrain improvements:', g.improvements.map((id) => { const im = rules.improvements[id]; const text = IMPROVEMENT_TEXT[id]; return text ? `${im.name} — ${text}` : im.name; })); section('Leads to further research:', g.prereqOf.map((id) => rules.techs[id].name)); if (tech.repeatable) { lines.push({ text: 'Repeatable — each additional discovery adds to your score.', color: COLORS.mutedHex }); } return { title: tech.name, lines }; }