diff --git a/README.md b/README.md index 1c7d6ee..46ed7fd 100644 --- a/README.md +++ b/README.md @@ -55,15 +55,21 @@ Click the map to give it keyboard focus. | Escape | Leave the cottage or bench while the map has focus | | F / Fullscreen | Fill the browser window at the current map scale; use Exit fullscreen or Escape to return | | + / − or zoom buttons | Zoom the Townmap between 50% and 200% | -| 0 / percentage button | Reset to the initial map scale (100%) | +| Town / 0 / percentage button | Compact view at one CSS pixel per artwork pixel (100%) | +| Walk | A closer, crisp 2× view centred on Jack, without moving him | +| V | Switch between Town and Walk while the map has focus | | Overview | Frame working and blocked cottages, including active roommates and shed residents | | Evidence / Close | Show or hide the shared inspector in any view | | Click a cottage or its roster button | Open its inspector without walking there | +| Hover a cottage | Read its name, status, harness and current work without enlarging the map | | Enter cottage / Leave cottage | Visit or exit using buttons | | Click a resident / Talk to agent | Open their conversation and recorded activity | | Click a room object or inspector tab | Read the request, clock, journal, to-do list, PR desk, or shelves | | Click a parcel | Open its PR when a URL is available | +Town starts at 100% on every screen: a cottage is 54 × 66 CSS pixels. Walk uses +200% for a closer view. These scales stay the same when the window, evidence +drawer, or fleet size changes; they are not a percentage of the initial panel width. Fullscreen fills the browser window and shows more map without enlarging cottages. Browser tabs stay visible; it never requests native fullscreen or a macOS space. Details float on @@ -78,7 +84,7 @@ blocked residents, including roommates and children. Existing plots stay fixed through feed updates, fullscreen and resizing; arrivals fill spare lots or add an annex. Overview changes the camera, with the Details panel taken into account, and falls back to all visible cottages when none are active. It respects the -current filters and the 50% zoom floor, so very large fleets still need scrolling. +current filters and can fit below the ordinary 50% manual zoom floor. Branch labels express grouping, not inferred Git ancestry. Rooms and residents are generated from cottage and task identity. Return visits keep the furniture in place. A new explicit task gets a new home. HubTown sets the cozy base with switchboards and pigeonholes. AppTown fills its desks with computers, phones and charging leads. MemTown lines its walls with bookshelves. VaultTown is a clock-and-lock repair shop, with pendulums, key racks and scattered repair tools. Unknown projects get neutral homes. diff --git a/scripts/browser-viewport-smoke.mjs b/scripts/browser-viewport-smoke.mjs index e5de277..557fa98 100644 --- a/scripts/browser-viewport-smoke.mjs +++ b/scripts/browser-viewport-smoke.mjs @@ -45,6 +45,17 @@ try { // where a former fixed scroll offset would legitimately clamp at the edge. await evaluate('window.originalPanel=document.getElementById("panel"); window.nativeCalls=[]; document.getElementById("map-shell").requestFullscreen=()=>{nativeCalls.push("request");return Promise.resolve();}; document.exitFullscreen=()=>{nativeCalls.push("exit");return Promise.resolve();}; window.originalWindow=[innerWidth,innerHeight]; const v=document.getElementById("map-viewport"); v.scrollTop=Math.max(0,(window.cottageState().viewport.geometry.mapHeight-v.clientHeight)/2);'); const normal = await state(); + near(normal.viewport.scale,1,'Native Town scale',1e-8); + const beforeWalk=await evaluate('window.cottageState().scene.player'); + await click('#camera-walk'); + near((await state()).viewport.scale,2,'Walk scale',1e-8); + assert.deepEqual(await evaluate('window.cottageState().scene.player'),beforeWalk,'Walk frames Jack without teleporting him'); + assert.equal(await evaluate('document.getElementById("camera-walk").getAttribute("aria-pressed")'),'true'); + await key('v'); + near((await state()).viewport.scale,1,'V returns to Town scale',1e-8); + // Restore the original center before exercising fullscreen's preservation. + await evaluate(`document.getElementById('map-viewport').scrollTop=${normal.viewport.center.y}-document.getElementById('map-viewport').clientHeight/2`); + pass('Town and Walk use native and double pixels without moving Jack'); await click('#fullscreen-toggle'); await until('window.cottageState().display.fullscreen','In-window fullscreen did not open'); const full = await state(); @@ -164,6 +175,7 @@ try { frame.remove();return result; })()`); near(result.scale,result.initial,'Responsive fullscreen scale',1e-8); + near(result.initial,1,'Responsive startup uses native pixels',1e-8); assert.equal(result.overflow,false); assert.equal(result.closeVisible,true); assert.ok(result.panel.left>=0 && result.panel.right<=result.width); assert.ok(result.panel.top>=0 && result.panel.bottom<=result.height); diff --git a/src/map-display.mjs b/src/map-display.mjs index 502a2be..9f1022f 100644 --- a/src/map-display.mjs +++ b/src/map-display.mjs @@ -2,13 +2,14 @@ import {fullscreenButtonCopy} from './fullscreen.mjs'; import {nextZoom, zoomKey, ZOOM_LEVELS} from './viewport.mjs'; /** Fullscreen and panel chrome live outside the inspector's streaming content. */ -export function createMapDisplay({camera, canvas, getMode, onOverview, getWorkspaceView=()=> 'town', focusWorkspace}) { +export function createMapDisplay({camera, canvas, getMode, getWalkPoint, onOverview, getWorkspaceView=()=> 'town', focusWorkspace}) { const $ = id => document.getElementById(id); const root = $('map-shell'), viewport = $('map-viewport'), details = $('details-shell'); const panel = $('panel'); const outside = [...root.parentElement.children].filter(element => element !== root); const fullscreen = $('fullscreen-toggle'), toggle = $('details-toggle'), close = $('details-close'); const zoomControls = $('map-zoom'), minus = $('zoom-out'), plus = $('zoom-in'), reset = $('zoom-reset'); + const townView = $('camera-town'), walkView = $('camera-walk'); let active = false, detailsOpen = true, pageScroll = null; let frame = 0; let fullscreenReading = null; @@ -70,12 +71,23 @@ export function createMapDisplay({camera, canvas, getMode, onOverview, getWorksp const zoom = camera.state.zoom; zoomControls.hidden = getMode() === 'room'; minus.disabled = zoom <= ZOOM_LEVELS[0]; plus.disabled = zoom === ZOOM_LEVELS.at(-1); - reset.textContent = zoom + '%'; reset.setAttribute('aria-label', 'Map zoom ' + zoom + '%. Reset to 100%'); + reset.textContent = zoom + '%'; reset.setAttribute('aria-label', 'Map zoom ' + zoom + '%. Reset to Town view'); + townView.setAttribute('aria-pressed', String(camera.state.view === 'town')); + walkView.setAttribute('aria-pressed', String(camera.state.view === 'walk')); + } + function changeView(view) { + camera.setView(view, view === 'walk' ? getWalkPoint?.() : undefined); + syncZoom(); + $('overview-status').textContent = view === 'walk' ? 'Walk view. Camera follows Jack at 200%.' : 'Town view at 100%. Hover cottages for details.'; } function changeZoom(action) { + if (action === 'view') { changeView(camera.state.view === 'walk' ? 'town' : 'walk'); return; } camera.setZoom(action === 'reset' ? 100 : nextZoom(camera.state.zoom, action === 'in' ? 1 : -1)); syncZoom(); } + for (const [button, view] of [[townView, 'town'], [walkView, 'walk']]) { + button.addEventListener('click', () => { changeView(view); focusMap(); }); + } for (const [button, action] of [[minus, 'out'], [plus, 'in'], [reset, 'reset']]) { button.addEventListener('click', () => { changeZoom(action); focusMap(); }); } diff --git a/src/observatory.mjs b/src/observatory.mjs index 9f02ac5..2828c2d 100644 --- a/src/observatory.mjs +++ b/src/observatory.mjs @@ -783,7 +783,7 @@ export function createObservatory(api){ } if(followId)hint('On the bench with '+(byId(followId)?.name||'your agent')+' · Move or press Escape to leave'); else if(mode==='town'&&gramophone&&distance(player,gramophone)<24)hint('E · '+(extras.state.music.enabled?'Stop':'Play')+' the village gramophone'); - else if(mode==='town')hint('Arrow keys / WASD to walk · E to talk · + / − zoom · 0 reset · F fullscreen · Click to inspect'); + else if(mode==='town')hint('WASD / arrows to walk · E to talk · V Town / Walk · + / − zoom · 0 Town · F fullscreen · Hover or click cottages'); } function draw(time){ const dt=Math.min(.05,lastFrame?time-lastFrame:1/60);lastFrame=time; diff --git a/src/town-layout.mjs b/src/town-layout.mjs index 4df359d..c46b134 100644 --- a/src/town-layout.mjs +++ b/src/town-layout.mjs @@ -1,11 +1,10 @@ -/** The old two-district width remains the reference for 100% cottage scale. */ +/** Minimum packing width; camera scale is independent of district layout. */ export const MAP_REFERENCE_WIDTH = 768; /** Choose a packing budget once. Floating Details must not permanently narrow the world. */ -export function townWidthBudget({windowWidth, standardWidth}) { - const scale = Math.max(1, standardWidth) / MAP_REFERENCE_WIDTH; +export function townWidthBudget({windowWidth}) { const clearWidth = windowWidth - 24; - return Math.round(Math.max(MAP_REFERENCE_WIDTH, Math.min(1920, clearWidth / scale))); + return Math.round(Math.max(MAP_REFERENCE_WIDTH, Math.min(1920, clearWidth))); } const townOf = agent => agent.town || agent.role || 'WildTown'; diff --git a/src/town.html b/src/town.html index 098c0ae..35cd6d4 100644 --- a/src/town.html +++ b/src/town.html @@ -115,13 +115,19 @@ #room-canvas{position:absolute;top:0;left:0;width:100%;height:100%;background:#171e22} .map-viewport.is-indoors{overflow:hidden} .map-viewport.is-indoors .map-surface{display:none} - .map-zoom{display:inline-flex;gap:2px;align-items:center} + .map-zoom{display:inline-flex;gap:2px;align-items:center;flex-wrap:wrap} .map-zoom button{min-width:34px;padding:8px} + #camera-walk{margin-right:6px} #zoom-reset{min-width:62px;font-variant-numeric:tabular-nums} .details-shell{min-width:0;min-height:0} .details-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 12px;border:1px solid #44524f;border-bottom:0;background:#273238} .split.details-closed{grid-template-columns:minmax(0,1fr)} #perspective-stage{position:relative;min-width:0;min-height:0} + .cottage-peek{position:absolute;z-index:5;width:270px;max-width:calc(100% - 16px);padding:9px 11px;border:1px solid #9eaa83;border-radius:4px;background:#202e2bf5;box-shadow:0 4px 16px #101b2255;pointer-events:none;font:12px/1.4 ui-sans-serif,system-ui;color:#e4ebe2;overflow-wrap:anywhere} + .cottage-peek strong,.cottage-peek span{display:block} + .cottage-peek strong{color:#e8d39b;font-size:13px} + .cottage-peek span{color:#b5c6b8;margin-top:2px} + .cottage-peek p{margin:5px 0 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} #command-view{position:absolute;inset:0;overflow:auto;outline:none;border-radius:3px} #command-view[hidden],.details-shell[hidden]{display:none!important} .split.is-fullscreen{position:fixed;inset:0;width:100%;height:100dvh;max-width:none;display:block;overflow:hidden;z-index:100;background:#171d23} @@ -262,9 +268,11 @@
Arrow keys / WASD to walk · Walk into doors · E to talk · F for fullscreen · Click to inspect