fix: draw street map ground and repair mobile layout
CI/CD / test (push) Successful in 57s
CI/CD / publish (push) Successful in 1m8s
CI/CD / deploy (push) Successful in 6s

- Turn the satellite ground off behind SATELLITE_GROUND so OSM streets,
  parks and land use are drawn; the imagery index is no longer fetched.
- Restore phone rules lost in the CSS-to-JS theme migration (#rank
  height, toolbar button size) and replace invalid -var() values.
- Add a landscape-phone layout and make the briefing scrollable.
- Add two-finger pinch zoom to the big map.
- Add regression tests for the theme and the skipped imagery request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 07:51:40 +03:30
co-authored by Claude Opus 5.5
parent df930694f7
commit 526796126d
9 changed files with 200 additions and 27 deletions
+81 -3
View File
@@ -138,10 +138,13 @@ export const gameTheme = {
'font-size': 'var(--game-token-11)',
'letter-spacing': 'var(--game-token-12)',
},
// The briefing can be taller than a phone screen (portrait text, or any landscape phone), so it
// scrolls; otherwise the start and install buttons end up below the edge, out of reach.
'#briefing': {
position: 'fixed',
inset: '0',
'z-index': '40',
'overflow-y': 'auto',
display: 'grid',
'grid-template-columns': '1.3fr 1fr',
background:
@@ -250,7 +253,7 @@ export const gameTheme = {
},
'.map-hint': {
position: 'absolute',
bottom: '-var(--game-token-17)',
bottom: 'calc(-1 * var(--game-token-17))',
left: '50%',
transform: 'translateX(-50%)',
padding: 'var(--game-token-35) var(--game-token-36)',
@@ -348,7 +351,7 @@ export const gameTheme = {
left: '50%',
width: 'var(--game-token-17)',
height: 'var(--game-token-17)',
margin: '-var(--game-token-35)',
margin: 'calc(-1 * var(--game-token-35))',
'border-radius': '50%',
background: 'var(--game-token-44)',
'pointer-events': 'none',
@@ -918,6 +921,7 @@ export const gameTheme = {
display: 'none',
},
'#status': {
right: 'var(--game-token-30)',
bottom: 'var(--game-token-24)',
},
'.stat': {
@@ -927,8 +931,11 @@ export const gameTheme = {
'.stat strong': {
'font-size': 'var(--game-token-42)',
},
// The rank bar moves to the top on phones; without bottom: auto it would stretch to full height.
'#rank': {
top: 'var(--game-token-17)',
bottom: 'auto',
width: '70vw',
padding: 'var(--game-token-35) var(--game-token-45)',
'font-size': 'var(--game-token-37)',
},
@@ -945,6 +952,8 @@ export const gameTheme = {
'padding-bottom': 'env(safe-area-inset-bottom, var(--game-token-9))',
},
'#toolbar button': {
padding: 'var(--game-token-17) var(--game-token-45)',
'font-size': 'var(--game-token-37)',
flex: '0 0 auto',
'white-space': 'nowrap',
},
@@ -973,11 +982,80 @@ export const gameTheme = {
bottom: 'var(--game-token-78)',
},
},
// Phones held sideways: the desktop layout is too tall for ~390px, so shrink the map and panels,
// move the rank bar to the top and lay the touch buttons out in a row above the stats.
// Only height is tested, so every phone held sideways matches whatever its width; narrow ones
// also match the phone block above, and this block, coming later, wins.
'@media (max-height: 520px)': {
'.brief-panel': {
'align-self': 'start',
margin: 'var(--game-token-42)',
padding: 'var(--game-token-11)',
},
'#brief-text': {
'font-size': 'var(--game-token-41)',
},
'#minimap-button,\n #minimap': {
width: 'var(--game-token-72)',
height: 'var(--game-token-72)',
},
'#minimap-button': {
top: 'var(--game-token-30)',
left: 'var(--game-token-30)',
},
'#location': {
top: 'calc(var(--game-token-72) + var(--game-token-42))',
left: 'var(--game-token-30)',
width: 'var(--game-token-72)',
},
'#objectives': {
top: 'var(--game-token-30)',
right: 'var(--game-token-30)',
width: '30vw',
padding: 'var(--game-token-45)',
},
'#mission-list li:not(.active)': {
display: 'none',
},
'#rank': {
top: 'var(--game-token-17)',
bottom: 'auto',
width: '40vw',
padding: 'var(--game-token-35) var(--game-token-45)',
'font-size': 'var(--game-token-37)',
},
'#rank .bar': {
height: 'var(--game-token-21)',
'margin-top': 'var(--game-token-35)',
},
'#compass': {
top: 'var(--game-token-89)',
width: '34vw',
},
'.stat': {
'min-width': 'var(--game-token-81)',
padding: 'var(--game-token-17) var(--game-token-45)',
},
'.stat strong': {
'font-size': 'var(--game-token-42)',
},
'#toolbar button': {
padding: 'var(--game-token-17) var(--game-token-45)',
'font-size': 'var(--game-token-37)',
},
'#touch': {
bottom: 'var(--game-token-24)',
},
'.touch-buttons': {
'grid-auto-flow': 'column',
'margin-bottom': 'var(--game-token-69)',
},
},
'.brief-robot': {
float: 'left',
width: 'var(--game-token-82)',
height: 'auto',
margin: '-var(--game-token-45) 0 0 var(--game-token-45)',
margin: 'calc(-1 * var(--game-token-45)) 0 0 var(--game-token-45)',
filter: 'drop-shadow(0 var(--game-token-45) var(--game-token-11) var(--game-token-83))',
},
'.brief-actions': {
+34
View File
@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { gameTheme } from './gameTheme.js';
/**
* Collects every CSS value in the theme, including those inside media blocks.
* @param {object} rules - Theme rules keyed by selector or at-rule.
* @returns {string[]} All declaration values.
*/
function allValues(rules) {
return Object.values(rules).flatMap((body) =>
Object.values(body).flatMap((value) =>
typeof value === 'object' ? allValues({ value }) : [value],
),
);
}
describe('game theme', () => {
it('uses no negated var(), which browsers drop as invalid', () => {
expect(allValues(gameTheme).filter((value) => String(value).includes('-var('))).toEqual([]);
});
it('keeps the phone rank bar at the top instead of stretching it to full height', () => {
expect(gameTheme['@media (max-width: 760px)']['#rank']).toMatchObject({ bottom: 'auto' });
});
it('keeps the landscape phone layout as a top-level media block', () => {
expect(gameTheme['@media (max-height: 520px)']?.['#minimap-button']).toBeDefined();
expect(gameTheme['@media (max-width: 760px)']['@media (max-height: 520px)']).toBeUndefined();
});
it('lets the briefing scroll so its buttons stay reachable on short screens', () => {
expect(gameTheme['#briefing']['overflow-y']).toBe('auto');
});
});