116 lines
6.3 KiB
JavaScript
116 lines
6.3 KiB
JavaScript
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 },
|
||
};
|
||
}
|