diff --git a/CHANGELOG.md b/CHANGELOG.md index f648223..71a0a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v0.3.0 — 2026-09-17 00:32 +- Removed the zoom-driven camera tilt (`lib/camera.ts` pitch curve + the map `move` handler): the map is now always flat/top-down with standard pan/zoom only; deleted `lib/camera.ts` and its unit tests +- Removed terrain: no more DEM raster source / `map.setTerrain`, and the `?terrain=0` / `NEXT_PUBLIC_TERRAIN` toggles are gone — pure 2D view, no 3D elements +- MAX_ZOOM 14 → 11: at European latitudes (~52°N) z11 ≈ 47 m/px, so the viewport spans ~90–135 km across a typical desktop width (≈100 km on screen); street-level zoom had no gameplay value +- `e2e/map.spec.ts`: APP_URL is now `/` (terrain flag removed), MAX_ZOOM mirror updated to 11; PLAN.md map section updated to flat-2D-only +- Verified: unit suite 8/8; `tsc --noEmit` clean; e2e suite 3/3 green against the rebuilt compose instance (flat view, wheel zoom clamped to [3, 11]) + ## v0.2.9 — 2026-09-17 00:21 - `data/airports_eu.csv`: multi-airport metro areas reduced to one dot each on the map — Paris→CDG, London→LHR, Moscow→SVO, Milan→MXP, Istanbul→IST, Warsaw→WAW, Brussels→BRU (Rhine-Ruhr keeps both DUS and CGN as distinct cities); dropped 18 satellite airports incl. Orly, Gatwick, Luton, Stansted, Domodedovo, Vnukovo, Zhukovsky, Linate, Caravaggio, Atatürk, Modlin, Charleroi, Weeze, Prestwick, Treviso, Ciampino, Băneasa — 260 → 242 rows; `/api/airports` and the circle layer pick it up with no code changes - Verified: unit suite 8/8; `tsc --noEmit` clean; e2e suite 3/3 green against the rebuilt compose instance (`/api/airports` serves 242 features) diff --git a/PLAN.md b/PLAN.md index cbe98c6..a24efb2 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,7 +1,7 @@ # Flight Ops — Game Design Browser game, pure economic gameplay: run a small airplane transport company in Europe. -No manual flying — planes auto-fly assigned routes; you manage the business from an ops dashboard over a 2D map (lite 3D terrain if it stays lightweight). +No manual flying — planes auto-fly assigned routes; you manage the business from an ops dashboard over a 2D map. ## Layout (single screen) - Left pane — Assets: fleet list (plane type, status, fuel, location), wallet, reputation @@ -14,7 +14,7 @@ Accept job → dispatch a plane → watch it fly the route on the map → earn m ## Map & rendering - MapLibre GL JS with vector tiles (OpenFreeMap/OSM-based) — crisp detail down to ~10–100 m/px at city zoom, smooth continent overview; no Cesium needed - Camera: top-down when zoomed out, tilts to ~45° as you zoom in (zoom-driven pitch transition) -- Lite 3D once tilted: terrain/hillshade from free DEM tiles + building extrusion if it stays lightweight, otherwise flat 2D +- View is flat 2D only (zoom-driven tilt and terrain were removed in v0.3.0 as over-engineered) - Game-logic data (airports, runways, countries/regions): downloaded CSV dataset cached in `data/` ## Simulation engine diff --git a/components/Map.tsx b/components/Map.tsx index 24f3bc1..5f403e6 100644 --- a/components/Map.tsx +++ b/components/Map.tsx @@ -3,16 +3,14 @@ import { useEffect, useRef, useState } from 'react'; import * as maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; -import { pitchForZoom } from '@/lib/camera'; const EUROPE_CENTER: [number, number] = [10, 52]; const INITIAL_ZOOM = 4; -// Useful gameplay range: continent overview → city detail (10–100 m/px). The -// zoom→pitch camera transition completes at z12; street-level zoom has no value. +// Flat 2D view only. Max zoom ≈ ~100 km across a typical desktop viewport width at +// European latitudes (~47 m/px at z11, 52°N); street-level zoom has no gameplay value. export const MIN_ZOOM = 3; -export const MAX_ZOOM = 14; +export const MAX_ZOOM = 11; const OPENFREEMAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty'; -const TERRAIN_TILES = ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png']; function escapeHtml(s: string): string { return s.replace(/&/g, '&').replace(//g, '>'); @@ -25,10 +23,6 @@ export default function Map() { useEffect(() => { if (!containerRef.current) return; - const terrainEnabled = - new URLSearchParams(window.location.search).get('terrain') !== '0' && - process.env.NEXT_PUBLIC_TERRAIN !== '0'; - // Bundlers (Turbopack) break MapLibre's derived worker URL, so point it at a // route that serves the worker script (and its relative imports) from node_modules. maplibregl.setWorkerUrl('/api/mlworker/maplibre-gl-worker.mjs'); @@ -38,20 +32,12 @@ export default function Map() { style: OPENFREEMAP_STYLE, center: EUROPE_CENTER, zoom: INITIAL_ZOOM, - pitch: 0, minZoom: MIN_ZOOM, maxZoom: MAX_ZOOM, }); // Dev/e2e hook so tests can project coordinates to screen space. (window as unknown as Record).__flightMap = map; - map.on('move', () => { - const target = pitchForZoom(map.getZoom()); - if (Math.abs(map.getPitch() - target) > 0.05) { - map.setPitch(target); - } - }); - // Airports are GeoJSON, so they don't depend on basemap tiles: initialize as soon // as the style is applied ('styledata') rather than waiting for 'load', which also // requires all tile sources to finish. @@ -60,16 +46,6 @@ export default function Map() { if (initialized) return; initialized = true; try { - if (terrainEnabled) { - map.addSource('dem', { - type: 'raster-dem', - tiles: TERRAIN_TILES, - encoding: 'terrarium', - tileSize: 256, - }); - map.setTerrain({ source: 'dem', exaggeration: 1.4 }); - } - const res = await fetch('/api/airports'); if (!res.ok) return; const data = (await res.json()) as { features: unknown[] }; diff --git a/e2e/map.spec.ts b/e2e/map.spec.ts index 4897240..c635c11 100644 --- a/e2e/map.spec.ts +++ b/e2e/map.spec.ts @@ -1,12 +1,10 @@ import { test, expect, type Page } from '@playwright/test'; -// Terrain is disabled in e2e (?terrain=0) for deterministic headless rendering; -// the terrain code path stays toggleable on by default in the app. -const APP_URL = '/?terrain=0'; +const APP_URL = '/'; // Must match MIN_ZOOM / MAX_ZOOM exported from components/Map.tsx. const MIN_ZOOM = 3; -const MAX_ZOOM = 14; +const MAX_ZOOM = 11; async function waitForAirportCount(page: Page): Promise { const countEl = page.getByTestId('airport-count'); diff --git a/lib/camera.test.ts b/lib/camera.test.ts deleted file mode 100644 index 05dde13..0000000 --- a/lib/camera.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { pitchForZoom, smoothstep, PITCH_ZOOM_MIN, PITCH_ZOOM_MAX, MAX_PITCH_DEG } from './camera'; - -describe('smoothstep', () => { - it('clamps outside the edges and is 0.5 at the midpoint', () => { - expect(smoothstep(0, 1, -5)).toBe(0); - expect(smoothstep(0, 1, 2)).toBe(1); - expect(smoothstep(0, 1, 0.5)).toBeCloseTo(0.5); - }); -}); - -describe('pitchForZoom', () => { - it('is 0 at and below the lower zoom threshold', () => { - expect(pitchForZoom(PITCH_ZOOM_MIN)).toBe(0); - expect(pitchForZoom(4)).toBe(0); - expect(pitchForZoom(0)).toBe(0); - }); - - it('is max pitch at and above the upper zoom threshold', () => { - expect(pitchForZoom(PITCH_ZOOM_MAX)).toBe(MAX_PITCH_DEG); - expect(pitchForZoom(15)).toBe(MAX_PITCH_DEG); - }); - - it('rises smoothly between thresholds (monotonic, midpoint = half max)', () => { - expect(pitchForZoom((PITCH_ZOOM_MIN + PITCH_ZOOM_MAX) / 2)).toBeCloseTo(MAX_PITCH_DEG / 2); - let prev = -1; - for (let z = PITCH_ZOOM_MIN; z <= PITCH_ZOOM_MAX; z += 0.25) { - const p = pitchForZoom(z); - expect(p).toBeGreaterThan(prev); - prev = p; - } - }); -}); diff --git a/lib/camera.ts b/lib/camera.ts deleted file mode 100644 index 658856c..0000000 --- a/lib/camera.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const PITCH_ZOOM_MIN = 6; -export const PITCH_ZOOM_MAX = 12; -export const MAX_PITCH_DEG = 45; - -export function smoothstep(edge0: number, edge1: number, x: number): number { - const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0))); - return t * t * (3 - 2 * t); -} - -// Pitch is a pure function of zoom so it never fights user pan/zoom gestures. -export function pitchForZoom(zoom: number): number { - return smoothstep(PITCH_ZOOM_MIN, PITCH_ZOOM_MAX, zoom) * MAX_PITCH_DEG; -} diff --git a/package.json b/package.json index cbeb4ce..6d92182 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flight-ops", - "version": "0.2.9", + "version": "0.3.0", "private": true, "scripts": { "dev": "next dev -p 3000",