fertig-classic-games/src/games/civilization/CivilizationTooltips.js

116 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { COLORS } from '../../config.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 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 — sharply reduces corruption and stores the seat of government.',
defensesea: (b) => `Multiplies this city's defense against naval attacks by ${b.value}.`,
};
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 };
}
// 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, civ, opponentsData, units) {
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 (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 {
const byName = new Map();
for (const u of units) {
const def = rules.units[u.type];
const entry = byName.get(def.name) ?? { count: 0, def };
entry.count += 1;
byName.set(def.name, entry);
}
lines.push({ text: `${units.length} units`, color: COLORS.goldHex });
for (const [name, { count, def }] of byName) {
lines.push({ text: `${count}× ${name} (A${def.attack} D${def.defense} M${def.move})` });
}
}
return {
title: civ.name,
titleColor: civ.color,
lines,
icon: { texture: 'opponents', frame: opData?.spriteIndex ?? 0, color: civ.color, label: civ.name },
};
}