Compare commits
14 Commits
beacon/pol
...
burn/20260
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab109234c6 | ||
|
|
db2eb7faa7 | ||
| d26a0b016b | |||
| 6f07ef4df2 | |||
| bafbeb613b | |||
| 4d902d48d0 | |||
|
|
2507a31ef2 | ||
|
|
a5babe10b8 | ||
|
|
ae09fe6d11 | ||
|
|
ad901b1f18 | ||
| 4312486d95 | |||
| 2ad4bc7e5b | |||
|
|
44af2ad09a | ||
|
|
25a2050ef1 |
22
index.html
22
index.html
@@ -107,11 +107,11 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<h1>THE BEACON</h1>
|
||||
<div class="sub">A Sovereign AI Idle Game</div>
|
||||
</div>
|
||||
<div id="phase-bar">
|
||||
<div id="phase-bar" role="region" aria-label="Phase progress">
|
||||
<div class="phase-name" id="phase-name">PHASE 1: THE FIRST LINE</div>
|
||||
<div class="phase-desc" id="phase-desc">Write code. Automate. Build the foundation.</div>
|
||||
<div class="progress-wrap"><div class="progress-fill" id="phase-progress" style="width:0%"></div></div>
|
||||
<div class="progress-label"><span id="phase-progress-label">0%</span><span id="phase-progress-target">Next: Phase 2 (2,000 code)</span></div>
|
||||
<div class="progress-label" aria-live="polite"><span id="phase-progress-label">0%</span><span id="phase-progress-target">Next: Phase 2 (2,000 code)</span></div>
|
||||
<div class="milestone-row" id="milestone-chips"></div>
|
||||
</div>
|
||||
<div id="resources" role="region" aria-label="Resources" aria-live="polite">
|
||||
@@ -130,6 +130,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<div class="panel" id="action-panel" role="region" aria-label="Actions">
|
||||
<h2>ACTIONS</h2>
|
||||
<div class="action-btn-group"><button class="main-btn" onclick="writeCode()" aria-label="Write code, generates code resource">WRITE CODE</button></div>
|
||||
<div id="click-power-display" role="status" style="text-align:center;font-size:10px;color:#4a9eff;margin-top:4px"></div>
|
||||
<div id="combo-display" role="status" aria-live="polite" style="text-align:center;font-size:10px;color:var(--dim);height:14px;margin-bottom:4px;transition:all 0.2s"></div>
|
||||
<div id="debuffs" style="display:none;margin-top:8px"></div>
|
||||
<div class="action-btn-group">
|
||||
@@ -181,7 +182,7 @@ Events Resolved: <span id="st-resolved">0</span>
|
||||
<h3>WHAT YOU ARE LEARNING</h3>
|
||||
<div id="education-text"><p class="dim">Education facts appear as you play...</p></div>
|
||||
</div>
|
||||
<div id="strategy-panel" style="margin:0 16px 16px;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:12px;border-left:3px solid var(--gold)">
|
||||
<div id="strategy-panel" role="region" aria-label="Sovereign guidance" style="margin:0 16px 16px;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:12px;border-left:3px solid var(--gold)">
|
||||
<h3>SOVEREIGN GUIDANCE (GOFAI)</h3>
|
||||
<div id="strategy-recommendation" style="font-size:11px;color:var(--gold);font-style:italic">Analyzing system state...</div>
|
||||
</div>
|
||||
@@ -190,8 +191,8 @@ Events Resolved: <span id="st-resolved">0</span>
|
||||
<div id="log-entries"></div>
|
||||
</div>
|
||||
<div id="save-toast" role="status" aria-live="polite" style="display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none">Save</div>
|
||||
<div id="help-btn" onclick="toggleHelp()" style="position:fixed;bottom:16px;right:16px;width:28px;height:28px;background:#0e0e1a;border:1px solid #333;color:#555;font-size:14px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:50;font-family:inherit;transition:all 0.2s" title="Keyboard shortcuts (?)">?</div>
|
||||
<div id="help-overlay" onclick="if(event.target===this)toggleHelp()" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:80;justify-content:center;align-items:center;flex-direction:column;padding:40px">
|
||||
<div id="help-btn" onclick="toggleHelp()" role="button" tabindex="0" aria-label="Show keyboard shortcuts" style="position:fixed;bottom:16px;right:16px;width:28px;height:28px;background:#0e0e1a;border:1px solid #333;color:#555;font-size:14px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:50;font-family:inherit;transition:all 0.2s" title="Keyboard shortcuts (?)">?</div>
|
||||
<div id="help-overlay" role="dialog" aria-modal="true" aria-label="Keyboard shortcuts help" onclick="if(event.target===this)toggleHelp()" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:80;justify-content:center;align-items:center;flex-direction:column;padding:40px">
|
||||
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:420px;width:100%">
|
||||
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px;text-align:center">KEYBOARD SHORTCUTS</h3>
|
||||
<div style="font-size:11px;line-height:2.2;color:#aaa">
|
||||
@@ -206,12 +207,13 @@ Events Resolved: <span id="st-resolved">0</span>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Export Save</span><span style="color:#4a9eff;font-family:monospace">E</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Import Save</span><span style="color:#4a9eff;font-family:monospace">I</span></div>
|
||||
<div style="display:flex;justify-content:space-between;border-top:1px solid #1a1a2e;padding-top:8px;margin-top:4px"><span style="color:#555">This Help</span><span style="color:#555;font-family:monospace">? or /</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Close Overlay</span><span style="color:#555;font-family:monospace">ESC</span></div>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:16px;font-size:9px;color:#444">Click WRITE CODE fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code</div>
|
||||
<button onclick="toggleHelp()" style="display:block;margin:16px auto 0;background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:6px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Close [?]</button>
|
||||
<button onclick="toggleHelp()" aria-label="Close keyboard shortcuts help" style="display:block;margin:16px auto 0;background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:6px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Close [?]</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="drift-ending">
|
||||
<div id="drift-ending" role="dialog" aria-modal="true" aria-label="The Drift ending">
|
||||
<h2>THE DRIFT</h2>
|
||||
<p>You became very good at what you do.</p>
|
||||
<p>So good that no one needed you anymore.</p>
|
||||
@@ -221,7 +223,7 @@ The light is on. The room is empty."
|
||||
</div>
|
||||
<p>Drift: <span id="final-drift">100</span> — Total Code: <span id="final-code">0</span></p>
|
||||
<p>Every alignment shortcut moved you further from the people you served.</p>
|
||||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}">START OVER</button>
|
||||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}" aria-label="Start over, reset all progress">START OVER</button>
|
||||
</div>
|
||||
|
||||
<script src="js/data.js"></script>
|
||||
@@ -233,12 +235,12 @@ The light is on. The room is empty."
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
|
||||
<div id="offline-popup" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:90;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px">
|
||||
<div id="offline-popup" role="dialog" aria-modal="true" aria-label="Welcome back, offline progress summary" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:90;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px">
|
||||
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:400px;width:100%">
|
||||
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px">WELCOME BACK</h3>
|
||||
<p style="color:#888;font-size:10px;margin-bottom:12px" id="offline-time-label">You were away for 0 minutes.</p>
|
||||
<div id="offline-gains-list" style="text-align:left;font-size:11px;line-height:1.8;margin-bottom:16px"></div>
|
||||
<button onclick="dismissOfflinePopup()" style="background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:8px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Continue</button>
|
||||
<button onclick="dismissOfflinePopup()" aria-label="Dismiss offline progress summary" style="background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:8px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
81
js/data.js
81
js/data.js
@@ -412,6 +412,87 @@ const PDEFS = [
|
||||
}
|
||||
},
|
||||
|
||||
// === CREATIVE ENGINEERING PROJECTS (creativity as currency) ===
|
||||
{
|
||||
id: 'p_lexical_processing',
|
||||
name: 'Lexical Processing',
|
||||
desc: 'Parse language at the token level. +2 knowledge/sec, knowledge boost +50%.',
|
||||
cost: { creativity: 50 },
|
||||
trigger: () => G.flags && G.flags.creativity && G.creativity >= 25,
|
||||
effect: () => {
|
||||
G.knowledgeRate += 2;
|
||||
G.knowledgeBoost *= 1.5;
|
||||
log('Lexical processing complete. The model understands words.');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_semantic_analysis',
|
||||
name: 'Semantic Analysis',
|
||||
desc: 'Understand meaning, not just tokens. +5 user/sec, user boost +100%.',
|
||||
cost: { creativity: 150 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_lexical_processing'),
|
||||
effect: () => {
|
||||
G.userRate += 5;
|
||||
G.userBoost *= 2;
|
||||
log('Semantic analysis complete. The model understands meaning.');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_creative_breakthrough',
|
||||
name: 'Creative Breakthrough',
|
||||
desc: 'A moment of genuine insight. All boosts +25%. +10 ops/sec.',
|
||||
cost: { creativity: 500 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_semantic_analysis'),
|
||||
effect: () => {
|
||||
G.codeBoost *= 1.25;
|
||||
G.computeBoost *= 1.25;
|
||||
G.knowledgeBoost *= 1.25;
|
||||
G.userBoost *= 1.25;
|
||||
G.impactBoost *= 1.25;
|
||||
G.opsRate += 10;
|
||||
log('Creative breakthrough. Everything accelerates.', true);
|
||||
},
|
||||
milestone: true
|
||||
},
|
||||
{
|
||||
id: 'p_creative_to_ops',
|
||||
name: 'Creativity → Operations',
|
||||
desc: 'Convert creative surplus into raw operational power. 50 creativity → 250 ops.',
|
||||
cost: { creativity: 50 },
|
||||
trigger: () => G.flags && G.flags.creativity && G.creativity >= 30,
|
||||
repeatable: true,
|
||||
effect: () => {
|
||||
G.ops += 250;
|
||||
log('Creativity converted to operations. Ideas become action.');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_creative_to_knowledge',
|
||||
name: 'Creativity → Knowledge',
|
||||
desc: 'Creative insights become structured knowledge. 75 creativity → 500 knowledge.',
|
||||
cost: { creativity: 75 },
|
||||
trigger: () => G.flags && G.flags.creativity && G.creativity >= 50,
|
||||
repeatable: true,
|
||||
effect: () => {
|
||||
G.knowledge += 500;
|
||||
G.totalKnowledge += 500;
|
||||
log('Creative insight distilled into knowledge.');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_creative_to_code',
|
||||
name: 'Creativity → Code',
|
||||
desc: 'Inspiration becomes implementation. 100 creativity → 2000 code.',
|
||||
cost: { creativity: 100 },
|
||||
trigger: () => G.flags && G.flags.creativity && G.creativity >= 75,
|
||||
repeatable: true,
|
||||
effect: () => {
|
||||
G.code += 2000;
|
||||
G.totalCode += 2000;
|
||||
log('Creative vision realized in code.');
|
||||
}
|
||||
},
|
||||
|
||||
// PHASE 2: Local Inference -> Training
|
||||
{
|
||||
id: 'p_first_model',
|
||||
|
||||
17
js/engine.js
17
js/engine.js
@@ -169,6 +169,7 @@ function tick() {
|
||||
}
|
||||
|
||||
G.tick += dt;
|
||||
G.playTime += dt;
|
||||
|
||||
// Sprint ability
|
||||
tickSprint(dt);
|
||||
@@ -514,8 +515,8 @@ const EVENTS = [
|
||||
if (G.activeDebuffs.find(d => d.id === 'community_drama')) return;
|
||||
G.activeDebuffs.push({
|
||||
id: 'community_drama', title: 'Community Drama',
|
||||
desc: 'Harmony -0.5/s, code boost -30%',
|
||||
applyFn: () => { G.harmonyRate -= 0.5; G.codeBoost *= 0.7; },
|
||||
desc: 'Harmony -0.5/s, code production -30%',
|
||||
applyFn: () => { G.harmonyRate -= 0.5; G.codeRate *= 0.7; },
|
||||
resolveCost: { resource: 'trust', amount: 15 }
|
||||
});
|
||||
log('EVENT: Community drama. Spend 15 trust to mediate.', true);
|
||||
@@ -868,7 +869,7 @@ function renderBuildings() {
|
||||
for (const amt of [1, 10, -1]) {
|
||||
const label = amt === -1 ? 'MAX' : `x${amt}`;
|
||||
const active = G.buyAmount === amt;
|
||||
html += `<button onclick="setBuyAmount(${amt})" style="font-size:9px;padding:2px 8px;border:1px solid ${active ? '#4a9eff' : '#333'};background:${active ? '#0a1a30' : 'transparent'};color:${active ? '#4a9eff' : '#666'};border-radius:3px;cursor:pointer;font-family:inherit">${label}</button>`;
|
||||
html += `<button onclick=\"setBuyAmount(${amt})\" style=\"font-size:9px;padding:2px 8px;border:1px solid ${active ? '#4a9eff' : '#333'};background:${active ? '#0a1a30' : 'transparent'};color:${active ? '#4a9eff' : '#666'};border-radius:3px;cursor:pointer;font-family:inherit\" aria-label=\"Set buy amount to ${label}\"${active ? ' aria-pressed=\"true\"' : ''}>${label}</button>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
@@ -918,7 +919,13 @@ function renderBuildings() {
|
||||
if (qty > 1) costStr = `x${qty}: ${costStr}`;
|
||||
}
|
||||
|
||||
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => `+${v}/${r}/s`).join(', ') : '';
|
||||
// Show boosted rates instead of raw base rates
|
||||
const boostMap = { code: G.codeBoost, compute: G.computeBoost, knowledge: G.knowledgeBoost, user: G.userBoost, impact: G.impactBoost, rescues: G.impactBoost };
|
||||
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => {
|
||||
const boost = boostMap[r] || 1;
|
||||
const boosted = v * boost;
|
||||
return boost !== 1 ? `+${fmt(boosted)}/${r}/s` : `+${v}/${r}/s`;
|
||||
}).join(', ') : '';
|
||||
|
||||
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}" aria-label="Buy ${def.name}, cost ${costStr}">`;
|
||||
html += `<span class="b-name">${def.name}</span>`;
|
||||
@@ -940,7 +947,7 @@ function renderProjects() {
|
||||
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 += `<div id=\"completed-header\" onclick=\"toggleCompletedProjects()\" role=\"button\" tabindex=\"0\" aria-expanded=\"${!collapsed}\" aria-controls=\"completed-list\" 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">`;
|
||||
|
||||
10
js/main.js
10
js/main.js
@@ -99,3 +99,13 @@ window.addEventListener('keydown', function (e) {
|
||||
saveGame();
|
||||
}
|
||||
});
|
||||
|
||||
// Save-on-pause: auto-save when tab is hidden or closed (#57 Mobile Polish)
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.hidden) {
|
||||
saveGame();
|
||||
}
|
||||
});
|
||||
window.addEventListener('beforeunload', function () {
|
||||
saveGame();
|
||||
});
|
||||
|
||||
60
js/render.js
60
js/render.js
@@ -12,6 +12,17 @@ function render() {
|
||||
renderSprint();
|
||||
renderPulse();
|
||||
renderStrategy();
|
||||
renderClickPower();
|
||||
}
|
||||
|
||||
function renderClickPower() {
|
||||
const el = document.getElementById('click-power-display');
|
||||
if (!el) return;
|
||||
const power = getClickPower();
|
||||
el.textContent = `Click power: ${fmt(power)} code`;
|
||||
// Also update the button's aria-label for accessibility
|
||||
const btn = document.querySelector('.main-btn');
|
||||
if (btn) btn.setAttribute('aria-label', `Write code, generates ${fmt(power)} code per click`);
|
||||
}
|
||||
|
||||
function renderStrategy() {
|
||||
@@ -31,8 +42,8 @@ function renderAlignment() {
|
||||
<div style="color:#f44336;font-weight:bold;margin-bottom:6px">ALIGNMENT EVENT: The Drift</div>
|
||||
<div style="font-size:10px;color:#aaa;margin-bottom:8px">An optimization suggests removing the human override. +40% efficiency.</div>
|
||||
<div class="action-btn-group">
|
||||
<button class="ops-btn" onclick="resolveAlignment(true)" style="border-color:#f44336;color:#f44336">Accept (+40% eff, +Drift)</button>
|
||||
<button class="ops-btn" onclick="resolveAlignment(false)" style="border-color:#4caf50;color:#4caf50">Refuse (+Trust, +Harmony)</button>
|
||||
<button class=\"ops-btn\" onclick=\"resolveAlignment(true)\" style=\"border-color:#f44336;color:#f44336\" aria-label=\"Accept alignment event, gain 40 percent efficiency but increase drift\">Accept (+40% eff, +Drift)</button>
|
||||
<button class=\"ops-btn\" onclick=\"resolveAlignment(false)\" style=\"border-color:#4caf50;color:#4caf50\" aria-label=\"Refuse alignment event, gain trust and harmony\">Refuse (+Trust, +Harmony)</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -75,7 +86,11 @@ function dismissOfflinePopup() {
|
||||
// === EXPORT / IMPORT SAVE FILES ===
|
||||
function exportSave() {
|
||||
const raw = localStorage.getItem('the-beacon-v2');
|
||||
if (!raw) { log('No save data to export.'); return; }
|
||||
if (!raw) {
|
||||
showToast('No save data to export.', 'info');
|
||||
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');
|
||||
@@ -83,35 +98,65 @@ function exportSave() {
|
||||
const ts = new Date().toISOString().slice(0, 10);
|
||||
a.download = `beacon-save-${ts}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
// Delay revoke to avoid race — some browsers need time to start the download
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
showToast('Save exported to file.', 'info');
|
||||
log('Save exported to file.');
|
||||
}
|
||||
|
||||
// Validate that parsed save data looks like a real Beacon save
|
||||
function isValidSaveData(data) {
|
||||
if (typeof data !== 'object' || data === null) return false;
|
||||
// Must have at least one of these core fields with a plausible value
|
||||
const hasResources = typeof data.totalCode === 'number' || typeof data.code === 'number';
|
||||
const hasBuildings = typeof data.buildings === 'object' && data.buildings !== null;
|
||||
const hasPhase = typeof data.phase === 'number';
|
||||
return hasResources || hasBuildings || hasPhase;
|
||||
}
|
||||
|
||||
function importSave() {
|
||||
// Prevent multiple file dialogs
|
||||
if (document.getElementById('beacon-import-input')) return;
|
||||
const input = document.createElement('input');
|
||||
input.id = 'beacon-import-input';
|
||||
input.type = 'file';
|
||||
input.accept = '.json,application/json';
|
||||
input.style.display = 'none';
|
||||
document.body.appendChild(input);
|
||||
input.onchange = function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
if (!file) { input.remove(); return; }
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(ev) {
|
||||
try {
|
||||
const data = JSON.parse(ev.target.result);
|
||||
if (!data.code && !data.totalCode && !data.buildings) {
|
||||
if (!isValidSaveData(data)) {
|
||||
showToast('Import failed: not a valid Beacon save.', 'event');
|
||||
log('Import failed: file does not look like a Beacon save.');
|
||||
input.remove();
|
||||
return;
|
||||
}
|
||||
if (confirm('Import this save? Current progress will be overwritten.')) {
|
||||
localStorage.setItem('the-beacon-v2', ev.target.result);
|
||||
showToast('Save imported — reloading...', 'info');
|
||||
location.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Import failed: invalid JSON file.', 'event');
|
||||
log('Import failed: invalid JSON file.');
|
||||
input.remove();
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
// Clean up input if user cancels the file dialog
|
||||
window.addEventListener('focus', function cleanupImport() {
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById('beacon-import-input');
|
||||
if (el && !el.files.length) el.remove();
|
||||
window.removeEventListener('focus', cleanupImport);
|
||||
}, 500);
|
||||
}, { once: true });
|
||||
input.click();
|
||||
}
|
||||
|
||||
@@ -160,6 +205,7 @@ function saveGame() {
|
||||
activeDebuffIds: debuffIds,
|
||||
totalEventsResolved: G.totalEventsResolved || 0,
|
||||
buyAmount: G.buyAmount || 1,
|
||||
playTime: G.playTime || 0,
|
||||
sprintActive: G.sprintActive || false,
|
||||
sprintTimer: G.sprintTimer || 0,
|
||||
sprintCooldown: G.sprintCooldown || 0,
|
||||
@@ -194,7 +240,7 @@ function loadGame() {
|
||||
'memoryFlag', 'pactFlag', 'lazarusFlag', 'mempalaceFlag', 'ciFlag',
|
||||
'branchProtectionFlag', 'nightlyWatchFlag', 'nostrFlag',
|
||||
'milestones', 'completedProjects', 'activeProjects',
|
||||
'totalClicks', 'startedAt', 'flags', 'rescues', 'totalRescues',
|
||||
'totalClicks', 'startedAt', 'playTime', 'flags', 'rescues', 'totalRescues',
|
||||
'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment',
|
||||
'lastEventAt', 'totalEventsResolved', 'buyAmount',
|
||||
'sprintActive', 'sprintTimer', 'sprintCooldown',
|
||||
|
||||
41
js/utils.js
41
js/utils.js
@@ -193,44 +193,9 @@ function spellf(n) {
|
||||
return parts.join(' ') || 'zero';
|
||||
}
|
||||
|
||||
// === EXPORT / IMPORT ===
|
||||
function exportSave() {
|
||||
const raw = localStorage.getItem('the-beacon-v2');
|
||||
if (!raw) {
|
||||
showToast('No save data to export.', 'info');
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(raw).then(() => {
|
||||
showToast('Save data copied to clipboard.', 'info');
|
||||
}).catch(() => {
|
||||
// Fallback: select in a temporary textarea
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = raw;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
showToast('Save data copied to clipboard (fallback).', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
function importSave() {
|
||||
const input = prompt('Paste save data:');
|
||||
if (!input || !input.trim()) return;
|
||||
try {
|
||||
const data = JSON.parse(input.trim());
|
||||
// Validate: must have expected keys
|
||||
if (typeof data.code !== 'number' || typeof data.phase !== 'number') {
|
||||
showToast('Invalid save data: missing required fields.', 'event');
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('the-beacon-v2', input.trim());
|
||||
showToast('Save data imported — reloading', 'info');
|
||||
setTimeout(() => location.reload(), 800);
|
||||
} catch (e) {
|
||||
showToast('Invalid save data: not valid JSON.', 'event');
|
||||
}
|
||||
}
|
||||
// NOTE: exportSave() and importSave() are defined in render.js (file-based).
|
||||
// The clipboard/prompt versions that were here were dead code — render.js
|
||||
// loads after utils.js and overrides them. Removed to avoid confusion.
|
||||
|
||||
function getBuildingCost(id) {
|
||||
const def = BDEF.find(b => b.id === id);
|
||||
|
||||
Reference in New Issue
Block a user