import { TestCase } from "./framework/test_case.js"; import { setupDom, teardownDom } from "./framework/dom.js"; import { MapView } from "../client/js/map_view.js"; import { CIVILISATIONS, PROTO_UNITS } from "../shared/data.js"; import { ECONOMIC_GRADIENT, ECONOMIC_MODES, economicColour, economicFraction, } from "../client/js/map_view/economic.js"; import { smallState, mapLayers } from "./framework/helpers.js"; function cssRgb(hex) { const value = hex.replace("#", ""); const r = parseInt(value.slice(0, 2), 16); const g = parseInt(value.slice(2, 4), 16); const b = parseInt(value.slice(4, 6), 16); return `rgb(${r}, ${g}, ${b})`; } function luminance(hex) { const value = hex.replace("#", ""); const r = parseInt(value.slice(0, 2), 16); const g = parseInt(value.slice(2, 4), 16); const b = parseInt(value.slice(4, 6), 16); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } export class MapModesTest extends TestCase { async test_economic_fraction_clamps_and_centres_a_flat_range() { this.assertApprox(economicFraction(0, 0, 10), 0, 1e-9, "the minimum is zero"); this.assertApprox(economicFraction(10, 0, 10), 1, 1e-9, "the maximum is one"); this.assertApprox(economicFraction(5, 0, 10), 0.5, 1e-9, "the middle is halfway"); this.assertApprox(economicFraction(-4, 0, 10), 0, 1e-9, "below the range clamps"); this.assertApprox(economicFraction(40, 0, 10), 1, 1e-9, "above the range clamps"); this.assertApprox(economicFraction(7, 7, 7), 0.5, 1e-9, "a flat range sits mid-scale"); } async test_economic_colour_runs_from_yellow_to_blue() { const low = economicColour(0, 0, 100); const high = economicColour(100, 0, 100); this.assertEqual(low, ECONOMIC_GRADIENT[0], "the poorest tile is the first stop"); this.assertEqual(high, ECONOMIC_GRADIENT[ECONOMIC_GRADIENT.length - 1], "the richest tile is the last stop"); this.assertGreater(luminance(low), luminance(high), "the scale darkens as the value rises, so magnitude reads without colour"); const mid = economicColour(50, 0, 100); this.assertTrue(/^#[0-9a-f]{6}$/.test(mid), `a valid colour: ${mid}`); } async test_every_economic_mode_has_a_label_and_formatter() { for (const id of ["gdp", "population"]) { const meta = ECONOMIC_MODES[id]; this.assertNotNull(meta, `${id} is catalogued`); this.assertTrue(typeof meta.label === "string" && meta.label.length > 0, "it has a label"); this.assertTrue(typeof meta.format === "function", "it has a formatter"); this.assertTrue(meta.format(1234).length > 0, "the formatter returns text"); } } async test_the_economic_legend_names_its_range() { const state = smallState(); const env = await setupDom(); const map = this.buildMap(env); try { map.applySnapshot(state.snapshot(0)); map.setMapMode("gdp"); const values = new Map(); for (const k in map.tiles) values.set(k, 1234); map.setEconomicData("gdp", values, 100, 1234); this.assertTrue(map.economic, "the GDP mode is active"); this.assertFalse(map.political, "the political fill is off"); this.assertFalse(env.$("#map-legend").hasClass("hidden"), "the legend is shown"); this.assertEqual(env.$("#map-legend-title").text(), "GDP per capita", "the legend is titled"); this.assertEqual(env.$("#map-legend-min").text(), "€100", "the legend shows the minimum"); this.assertEqual(env.$("#map-legend-max").text(), "€1 234", "the legend shows the maximum"); // Every land tile carries the top-of-range colour, so the fill is live. const top = economicColour(1234, 100, 1234); let painted = false; env.$("#layer-terrain .hex.terrain").each((_index, element) => { const value = element.style.backgroundColor; if (value === top || value === cssRgb(top)) painted = true; }); this.assertTrue(painted, "tiles are painted on the colour scale"); } finally { teardownDom(env); } } async test_switching_modes_repaints_and_resets_the_legend() { const state = smallState(); const env = await setupDom(); const map = this.buildMap(env); try { map.applySnapshot(state.snapshot(0)); // Terrain at this zoom is the default plain map. this.assertFalse(map.political, "terrain mode at close zoom is not political"); this.assertFalse(map.economic, "terrain mode is not economic"); map.setMapMode("political"); this.assertTrue(map.political, "political mode forces the political fill"); this.assertTrue(map.$world.hasClass("political"), "the world is flagged political"); this.assertTrue(env.$("#map-legend").hasClass("hidden"), "no economic legend"); map.setMapMode("population"); this.assertTrue(map.economic, "population mode is economic"); this.assertFalse(map.political, "political is off in an economic mode"); map.setMapMode("terrain"); this.assertFalse(map.economic, "terrain clears the economic fill"); this.assertTrue(env.$("#map-legend").hasClass("hidden"), "the legend is hidden again"); } finally { teardownDom(env); } } async test_fog_rebuilds_are_coalesced_between_frames() { const env = await setupDom(); const map = this.buildMap(env); try { let builds = 0; map.glRenderer = { buildKnowledge: () => { builds += 1; } }; map._fogBuiltAt = 0; map._fogDirty = false; map._refreshFog(); map._refreshFog(); map._refreshFog(); map._flushFog(); this.assertEqual(builds, 1, "many visibility changes rebuild the fog once"); map._refreshFog(); map._flushFog(); this.assertEqual(builds, 1, "a later change waits for the interval"); map._fogBuiltAt = 0; map._flushFog(); this.assertEqual(builds, 2, "the pending change is flushed once due"); } finally { teardownDom(env); } } async test_country_names_sit_below_every_icon_layer() { const env = await setupDom(); try { const order = env.$("#map-world .layer").map((_index, element) => element.id).get(); const names = order.indexOf("layer-political-labels"); this.assertGreaterOrEqual(names, 0, "the country-name layer exists"); for (const id of [ "layer-highlight", "layer-paths", "layer-targets", "layer-entities", "layer-labels", ]) { this.assertLess(names, order.indexOf(id), `${id} paints above the country names`); } } finally { teardownDom(env); } } async test_territory_changes_skip_the_fog_and_region_rebuilds() { const state = smallState(); const env = await setupDom(); const map = this.buildMap(env); const calls = []; try { map.applySnapshot(state.snapshot(0)); // Install the spy only after the world is built, so it replaces the GL // renderer rather than being asked to build the whole world itself. map.glRenderer = { buildPolitical: () => calls.push("political"), buildEconomic: () => calls.push("economic"), buildBorders: () => calls.push("borders"), buildKnowledge: () => calls.push("fog"), buildRegionOverlay: () => calls.push("region"), }; map.political = true; calls.length = 0; map._applyTerritory([[0, 0, 0], [1, 0, 1]]); this.assertTrue(calls.includes("political"), "the political fill follows ownership"); this.assertFalse(calls.includes("fog"), "a territory change does not rebuild the fog"); this.assertFalse(calls.includes("region"), "nor the region overlay"); calls.length = 0; map.mapMode = "terrain"; map.political = false; map.setMapMode("political"); this.assertTrue(calls.includes("fog"), "a mode change does rebuild the fog"); this.assertTrue(calls.includes("region"), "and the region overlay"); } finally { teardownDom(env); } } buildMap(env) { const map = new MapView(env.$("#map-viewport"), env.$("#map-world"), mapLayers(env.$)); map.$viewport.width = () => 800; map.$viewport.height = () => 600; map.setCatalogue(CIVILISATIONS, PROTO_UNITS); return map; } }