import { COLLECTIONS, EVENT_LABELS, EVENT_TYPES, TOOLBELT, commandTranscript, formatTime } from './shared.js'; function escapeHtml(value) { return String(value).replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character])); } function byId(root, id) { return root.getElementById(id); } function setText(root, id, value) { const element = byId(root, id); if (element) element.textContent = value; } function selectedDistrict(state) { return state.districts.find((district) => district.id === state.selectedDistrictId) || state.districts[0]; } function selectedFile(state) { const district = selectedDistrict(state); return district.files.find((file) => file.id === state.selectedFileId) || district.files[0]; } function eventMessage(event) { return event.detail?.message || EVENT_LABELS[event.type] || event.type; } function renderAgents(root, state) { const agentList = byId(root, 'agent-list'); if (!agentList) return; agentList.innerHTML = state.agents.map((agent) => `
  • ${agent.name.slice(0, 2)}${escapeHtml(agent.name)}${escapeHtml(agent.role)}${escapeHtml(agent.task)}● ${escapeHtml(agent.status)}
  • `).join(''); setText(root, 'active-agent-count', String(state.agents.length)); } function renderEvents(root, state) { const feed = byId(root, 'event-feed'); if (!feed) return; feed.innerHTML = state.events.slice(0, 7).map((event) => `
  • ${escapeHtml(eventMessage(event))}

  • `).join(''); setText(root, 'event-count', `${state.events.length} EVENTS`); } function renderToolbelt(root, state) { const toolbelt = byId(root, 'toolbelt'); if (!toolbelt) return; toolbelt.innerHTML = TOOLBELT.map((tool) => ``).join(''); } function renderStatus(root, state) { setText(root, 'repo-visibility', state.repository.visibility); setText(root, 'repo-branch', `BRANCH / ${state.repository.branch}`); setText(root, 'build-status', state.repository.build); setText(root, 'test-status', state.repository.tests); setText(root, 'deploy-status', state.repository.deployment); setText(root, 'world-clock', formatTime(state.tick)); setText(root, 'player-location', state.view === 'district' ? `INSIDE ${selectedDistrict(state).path}` : 'SPAWN PLAZA'); setText(root, 'feedback-line', state.feedback); const playerLevel = Math.min(99, 12 + Math.floor(state.tick / 3)); setText(root, 'player-level', `LEVEL ${playerLevel} DEV`); const xp = Math.min(100, 34 + state.tick * 2); const xpBar = byId(root, 'xp-fill'); if (xpBar) xpBar.style.width = `${xp}%`; setText(root, 'xp-label', `${xp} / 100 XP`); } function renderFiles(state) { const district = selectedDistrict(state); return `
    DIRECTORY${escapeHtml(district.path)}${escapeHtml(district.stat)}
    ${district.files.map((file) => ``).join('')}
    `; } function renderCommits(state) { const commits = state.repository.commits || []; return `
    HISTORY ROADRECENT COMMITS${commits.length} simulated events
    ${commits.map((commit) => ``).join('')}
    `; } function renderIssues(state) { const issues = state.repository.issues || []; return `
    QUEST BOARDOPEN ISSUESQuests waiting in the city
    ${issues.map((issue) => ``).join('')}
    `; } function renderPullRequests(state) { const prs = state.repository.pullRequests || []; return `
    MERGE GATEPULL REQUESTSDoors into the next branch
    ${prs.map((pr) => ``).join('')}
    `; } function renderBuild(state) { return `
    AUTOMATION CENTERBUILD & TESTAll values are simulated browser-local signals
    BUILD #812${escapeHtml(state.repository.build)}
    INSPECTORS${escapeHtml(state.repository.tests)}
    PAGES RUNWAY${escapeHtml(state.repository.deployment)}
    SOURCECOMPILETESTDEPLOY
    `; } function renderRecord(state) { const record = state.selectedRecord; if (!record) return renderBuild(state); if (record.type === 'commit') { const item = (state.repository.commits || []).find((candidate) => candidate.id === record.id); return `
    COMMIT EVENT${escapeHtml(item?.hash || record.id)}Historical event made physical
    ${escapeHtml(item?.message || 'Commit selected')}

    This simulated commit is a construction event. The repository builder routes its changed files into the city.

    author · ${escapeHtml(item?.author || 'builder')}
    `; } if (record.type === 'issue') { const item = (state.repository.issues || []).find((candidate) => candidate.id === record.id); return `
    ISSUE QUEST${escapeHtml(item?.title || record.id)}${escapeHtml(item?.status || 'OPEN')}
    Quest objective

    ${escapeHtml(item?.detail || 'Inspect the open issue in the quest district.')}

    marker active · browser-local simulation
    `; } const item = (state.repository.pullRequests || []).find((candidate) => candidate.id === record.id); return `
    PULL REQUEST DOOR${escapeHtml(item?.title || record.id)}${escapeHtml(item?.status || 'REVIEW')}
    Merge gate

    ${escapeHtml(item?.detail || 'The gate is waiting for review.')}

    checks passing · browser-local simulation
    `; } function renderInspector(root, state, nearby) { const panel = byId(root, 'inspector-content'); if (!panel) return; const district = selectedDistrict(state); const file = selectedFile(state); const collection = state.selectedCollection; let content = ''; if (state.activePanel === 'file' || (state.activePanel === 'collection' && collection === 'files')) { content = `
    ${escapeHtml(file.kind)} OBJECT

    ${escapeHtml(file.name)}

    ${escapeHtml(file.detail)}

    ${escapeHtml(file.code)}
    `; } else if (state.activePanel === 'record') { content = renderRecord(state); } else if (state.activePanel === 'collection') { content = collection === 'commits' ? renderCommits(state) : collection === 'issues' ? renderIssues(state) : collection === 'prs' ? renderPullRequests(state) : renderBuild(state); } else if (state.activePanel === 'inspector' || state.activePanel === 'world') { const canEnter = nearby?.district?.id === district.id && nearby.inRange; content = `
    ${state.view === 'district' ? 'CURRENT DISTRICT' : 'NEAREST DISTRICT'}

    ${escapeHtml(district.title)}

    ${escapeHtml(district.summary)}

    ${escapeHtml(district.path)}${escapeHtml(district.stat)}${Math.round((state.construction[district.id] || 0) * 100)}% BUILT
    ${district.files.slice(0, 3).map((item) => ``).join('')}
    `; } else { content = `
    ${escapeHtml(district.path)}

    Repository objects

    Select a building, tool, or activity to inspect the physical software around you.

    `; } panel.innerHTML = content; const nearbyPrompt = byId(root, 'nearby-prompt'); if (nearbyPrompt) { nearbyPrompt.innerHTML = nearby?.district && nearby.inRange ? `NEARBY${escapeHtml(nearby.district.name)}PRESS E / ENTER TO ENTER` : `WALKABLE CITY${escapeHtml(state.player.facing.toUpperCase())}WASD · ARROWS TO NAVIGATE`; nearbyPrompt.classList.toggle('is-ready', Boolean(nearby?.inRange)); } } function renderTerminal(root, state) { const overlay = byId(root, 'terminal-phone'); if (!overlay) return; overlay.classList.toggle('is-open', state.terminal.open); overlay.setAttribute('aria-hidden', String(!state.terminal.open)); const output = byId(root, 'terminal-output'); if (output) output.innerHTML = state.terminal.lines.map((line) => `
    ${escapeHtml(line)}
    `).join(''); const input = byId(root, 'terminal-input'); if (input && input.value !== state.terminal.command) input.value = ''; } function renderSearch(root) { const search = byId(root, 'repo-search'); if (!search) return; const query = search.value.trim().toLowerCase(); root.querySelectorAll('.world-building').forEach((building) => { building.classList.toggle('is-search-hidden', Boolean(query) && !building.textContent.toLowerCase().includes(query)); }); } export function renderHud(root, state, nearby) { renderAgents(root, state); renderEvents(root, state); renderToolbelt(root, state); renderStatus(root, state); renderInspector(root, state, nearby); renderTerminal(root, state); renderSearch(root); const drawer = byId(root, 'inspector-drawer'); if (drawer) drawer.classList.toggle('is-open', state.activePanel !== 'world' && state.activePanel !== 'terminal'); const shell = byId(root, 'game-shell'); if (shell) shell.dataset.view = state.view; setText(root, 'selected-tool-label', (TOOLBELT.find((tool) => tool.id === state.activeTool) || TOOLBELT[0]).name); } export function bindUi(root, dispatch) { root.addEventListener('click', (event) => { const target = event.target.closest('[data-action], [data-district-id]'); if (!target) return; const action = target.dataset.action || (target.dataset.districtId ? 'select-district' : ''); if (action === 'select-district') dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId: target.dataset.districtId }); if (action === 'open-inspector') dispatch({ type: EVENT_TYPES.OPEN_INSPECTOR }); if (action === 'enter-district') dispatch({ type: EVENT_TYPES.ENTER_DISTRICT, districtId: target.dataset.districtId }); if (action === 'close-inspector') dispatch({ type: EVENT_TYPES.CLOSE_INSPECTOR }); if (action === 'select-file') dispatch({ type: EVENT_TYPES.SELECT_FILE, fileId: target.dataset.fileId }); if (action === 'select-collection') dispatch({ type: EVENT_TYPES.SELECT_COLLECTION, collection: target.dataset.collection }); if (action === 'commit-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'commit', recordId: target.dataset.commitId }); if (action === 'issue-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'issue', recordId: target.dataset.issueId }); if (action === 'pr-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'pr', recordId: target.dataset.prId }); if (action === 'move') dispatch({ type: EVENT_TYPES.PLAYER_MOVE, dx: target.dataset.dx, dy: target.dataset.dy }); if (action === 'tool') { const tool = TOOLBELT.find((item) => item.id === target.dataset.toolId); dispatch({ type: EVENT_TYPES.USE_TOOL, toolId: target.dataset.toolId, message: tool?.verb ? `${tool.name}: ${tool.verb}.` : undefined }); } if (action === 'open-terminal') dispatch({ type: EVENT_TYPES.OPEN_TERMINAL }); if (action === 'close-terminal') dispatch({ type: EVENT_TYPES.CLOSE_TERMINAL }); if (action === 'command') dispatch({ type: EVENT_TYPES.RUN_COMMAND, command: target.dataset.command, lines: commandTranscript(target.dataset.command, window.gitWorldState || { selectedFileId: 'README.md' }) }); }); root.addEventListener('submit', (event) => { if (!event.target.matches('#terminal-form')) return; event.preventDefault(); const input = byId(root, 'terminal-input'); const command = input?.value.trim() || ''; if (command) dispatch({ type: EVENT_TYPES.RUN_COMMAND, command, lines: commandTranscript(command, window.gitWorldState || { selectedFileId: 'README.md' }) }); }); const search = byId(root, 'repo-search'); if (search) search.addEventListener('input', () => renderSearch(root)); }