beacon: add collapsible completed projects + export/import save files

- Completed research projects are now collapsed by default (click to expand),
  preventing panel clutter in mid/late game. Toggle state persists in saves.
- Export saves as JSON files with E key or Export button
- Import saves from file with I key or Import button (validates before loading)
- Ctrl+S keyboard shortcut for quick save
- Updated keybind hints in startup log
This commit is contained in:
Timmy-Sprint
2026-04-10 15:25:09 -04:00
parent f948ec9c5e
commit 5fc0ad7b22
2 changed files with 79 additions and 8 deletions

81
game.js
View File

@@ -1729,13 +1729,21 @@ function renderProjects() {
let html = '';
// Show completed projects
if (G.completedProjects) {
for (const id of G.completedProjects) {
const pDef = PDEFS.find(p => p.id === id);
if (pDef) {
html += `<div class="project-done">OK ${pDef.name}</div>`;
// Collapsible completed projects section
if (G.completedProjects && G.completedProjects.length > 0) {
const count = G.completedProjects.length;
const collapsed = G.projectsCollapsed !== false;
html += `<div id="completed-header" onclick="toggleCompletedProjects()" style="cursor:pointer;font-size:9px;color:#555;padding:4px 0;border-bottom:1px solid #1a2a1a;margin-bottom:4px;user-select:none">`;
html += `${collapsed ? '▶' : '▼'} COMPLETED (${count})</div>`;
if (!collapsed) {
html += `<div id="completed-list">`;
for (const id of G.completedProjects) {
const pDef = PDEFS.find(p => p.id === id);
if (pDef) {
html += `<div class="project-done">OK ${pDef.name}</div>`;
}
}
html += `</div>`;
}
}
@@ -1759,6 +1767,11 @@ function renderProjects() {
container.innerHTML = html;
}
function toggleCompletedProjects() {
G.projectsCollapsed = G.projectsCollapsed === false ? true : false;
renderProjects();
}
function renderStats() {
const set = (id, v, raw) => {
const el = document.getElementById(id);
@@ -2080,6 +2093,49 @@ function dismissOfflinePopup() {
if (el) el.style.display = 'none';
}
// === EXPORT / IMPORT SAVE FILES ===
function exportSave() {
const raw = localStorage.getItem('the-beacon-v2');
if (!raw) { log('No save data to export.'); return; }
const blob = new Blob([raw], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const ts = new Date().toISOString().slice(0, 10);
a.download = `beacon-save-${ts}.json`;
a.click();
URL.revokeObjectURL(url);
log('Save exported to file.');
}
function importSave() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,application/json';
input.onchange = function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(ev) {
try {
const data = JSON.parse(ev.target.result);
if (!data.code && !data.totalCode && !data.buildings) {
log('Import failed: file does not look like a Beacon save.');
return;
}
if (confirm('Import this save? Current progress will be overwritten.')) {
localStorage.setItem('the-beacon-v2', ev.target.result);
location.reload();
}
} catch (err) {
log('Import failed: invalid JSON file.');
}
};
reader.readAsText(file);
};
input.click();
}
// === SAVE / LOAD ===
function showSaveToast() {
const el = document.getElementById('save-toast');
@@ -2124,6 +2180,7 @@ function saveGame() {
sprintActive: G.sprintActive || false,
sprintTimer: G.sprintTimer || 0,
sprintCooldown: G.sprintCooldown || 0,
projectsCollapsed: G.projectsCollapsed !== false,
savedAt: Date.now()
};
@@ -2260,7 +2317,7 @@ function initGame() {
log('Click WRITE CODE or press SPACE to start.');
log('Build AutoCode for passive production.');
log('Watch for Research Projects to appear.');
log('Keys: SPACE=Code S=Code Sprint 1=Ops->Code 2=Ops->Compute 3=Ops->Knowledge 4=Ops->Trust B=Buy x1/x10/MAX');
log('Keys: SPACE=Code S=Sprint 1-4=Ops B=Buy x1/10/MAX E=Export I=Import Ctrl+S=Save');
}
window.addEventListener('load', function () {
@@ -2308,4 +2365,14 @@ window.addEventListener('keydown', function (e) {
else setBuyAmount(1);
}
if (e.code === 'KeyS') activateSprint();
if (e.code === 'KeyE') exportSave();
if (e.code === 'KeyI') importSave();
});
// Ctrl+S to save (must be on keydown to preventDefault)
window.addEventListener('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') {
e.preventDefault();
saveGame();
}
});

View File

@@ -130,7 +130,11 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<div id="sprint-label" style="font-size:9px;color:#666;margin-top:2px;text-align:center"></div>
</div>
<div id="alignment-ui" style="display:none"></div>
<button class="save-btn" onclick="saveGame()">Save Game</button>
<button class="save-btn" onclick="saveGame()">Save Game [Ctrl+S]</button>
<div style="display:flex;gap:4px;margin-top:4px">
<button class="save-btn" onclick="exportSave()" style="flex:1">Export [E]</button>
<button class="save-btn" onclick="importSave()" style="flex:1">Import [I]</button>
</div>
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button>
<h2>BUILDINGS</h2>
<div id="buildings"></div>