2026-04-11 01:32:27 +00:00
function render ( ) {
renderResources ( ) ;
renderPhase ( ) ;
renderBuildings ( ) ;
renderProjects ( ) ;
renderStats ( ) ;
updateEducation ( ) ;
renderAlignment ( ) ;
renderProgress ( ) ;
renderCombo ( ) ;
renderDebuffs ( ) ;
renderSprint ( ) ;
renderPulse ( ) ;
renderStrategy ( ) ;
2026-04-10 22:17:26 -04:00
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 ` ) ;
2026-04-11 01:32:27 +00:00
}
function renderStrategy ( ) {
if ( window . SSE ) {
window . SSE . update ( ) ;
const el = document . getElementById ( 'strategy-recommendation' ) ;
if ( el ) el . textContent = window . SSE . getRecommendation ( ) ;
}
}
function renderAlignment ( ) {
const container = document . getElementById ( 'alignment-ui' ) ;
if ( ! container ) return ;
if ( G . pendingAlignment ) {
container . innerHTML = `
< div style = "background:#1a0808;border:1px solid #f44336;padding:10px;border-radius:4px;margin-top:8px" >
< div style = "color:#f44336;font-weight:bold;margin-bottom:6px" > ALIGNMENT EVENT : The Drift < / d i v >
< div style = "font-size:10px;color:#aaa;margin-bottom:8px" > An optimization suggests removing the human override . + 40 % efficiency . < / d i v >
< div class = "action-btn-group" >
2026-04-11 00:25:01 -04:00
< 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 ) < / b u t t o n >
< 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 ) < / b u t t o n >
2026-04-11 01:32:27 +00:00
< / d i v >
< / d i v >
` ;
container . style . display = 'block' ;
} else {
container . innerHTML = '' ;
container . style . display = 'none' ;
}
}
// === OFFLINE GAINS POPUP ===
function showOfflinePopup ( timeLabel , gains , offSec ) {
const el = document . getElementById ( 'offline-popup' ) ;
if ( ! el ) return ;
const timeEl = document . getElementById ( 'offline-time-label' ) ;
if ( timeEl ) timeEl . textContent = ` You were away for ${ timeLabel } . ` ;
const listEl = document . getElementById ( 'offline-gains-list' ) ;
if ( listEl ) {
let html = '' ;
for ( const g of gains ) {
html += ` <div style="display:flex;justify-content:space-between;padding:2px 0;border-bottom:1px solid #111"> ` ;
html += ` <span style="color: ${ g . color } "> ${ g . label } </span> ` ;
html += ` <span style="color:#4caf50;font-weight:600">+ ${ fmt ( g . value ) } </span> ` ;
html += ` </div> ` ;
}
// Show offline efficiency note
html += ` <div style="color:#555;font-size:9px;margin-top:8px;font-style:italic">Offline efficiency: 50%</div> ` ;
listEl . innerHTML = html ;
}
el . style . display = 'flex' ;
}
function dismissOfflinePopup ( ) {
const el = document . getElementById ( 'offline-popup' ) ;
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 ( ) ;
2026-04-11 00:25:01 -04:00
// Delay revoke to avoid race — some browsers need time to start the download
setTimeout ( ( ) => URL . revokeObjectURL ( url ) , 1000 ) ;
2026-04-11 01:32:27 +00:00
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' ) ;
if ( ! el ) return ;
const elapsed = Math . floor ( ( Date . now ( ) - G . startedAt ) / 1000 ) ;
const m = Math . floor ( elapsed / 60 ) ;
const s = elapsed % 60 ;
el . textContent = ` Saved [ ${ m } : ${ s . toString ( ) . padStart ( 2 , '0' ) } ] ` ;
el . style . display = 'block' ;
void el . offsetHeight ;
el . style . opacity = '1' ;
setTimeout ( ( ) => { el . style . opacity = '0' ; } , 1500 ) ;
setTimeout ( ( ) => { el . style . display = 'none' ; } , 2000 ) ;
}
/ * *
* Persists the current game state to localStorage .
* /
function saveGame ( ) {
// Save debuff IDs (can't serialize functions)
const debuffIds = ( G . activeDebuffs || [ ] ) . map ( d => d . id ) ;
const saveData = {
version : 1 ,
code : G . code , compute : G . compute , knowledge : G . knowledge , users : G . users , impact : G . impact ,
ops : G . ops , trust : G . trust , creativity : G . creativity , harmony : G . harmony ,
totalCode : G . totalCode , totalCompute : G . totalCompute , totalKnowledge : G . totalKnowledge ,
totalUsers : G . totalUsers , totalImpact : G . totalImpact ,
buildings : G . buildings ,
codeBoost : G . codeBoost , computeBoost : G . computeBoost , knowledgeBoost : G . knowledgeBoost ,
userBoost : G . userBoost , impactBoost : G . impactBoost ,
milestoneFlag : G . milestoneFlag , phase : G . phase ,
deployFlag : G . deployFlag , sovereignFlag : G . sovereignFlag , beaconFlag : G . beaconFlag ,
memoryFlag : G . memoryFlag , pactFlag : G . pactFlag ,
lazarusFlag : G . lazarusFlag || 0 , mempalaceFlag : G . mempalaceFlag || 0 , ciFlag : G . ciFlag || 0 ,
branchProtectionFlag : G . branchProtectionFlag || 0 , nightlyWatchFlag : G . nightlyWatchFlag || 0 ,
nostrFlag : G . nostrFlag || 0 ,
milestones : G . milestones , completedProjects : G . completedProjects , activeProjects : G . activeProjects ,
totalClicks : G . totalClicks , startedAt : G . startedAt ,
flags : G . flags ,
rescues : G . rescues || 0 , totalRescues : G . totalRescues || 0 ,
drift : G . drift || 0 , driftEnding : G . driftEnding || false , beaconEnding : G . beaconEnding || false , pendingAlignment : G . pendingAlignment || false ,
lastEventAt : G . lastEventAt || 0 ,
activeDebuffIds : debuffIds ,
totalEventsResolved : G . totalEventsResolved || 0 ,
buyAmount : G . buyAmount || 1 ,
2026-04-11 15:09:29 -04:00
playTime : G . playTime || 0 ,
2026-04-11 01:32:27 +00:00
sprintActive : G . sprintActive || false ,
sprintTimer : G . sprintTimer || 0 ,
sprintCooldown : G . sprintCooldown || 0 ,
swarmFlag : G . swarmFlag || 0 ,
swarmRate : G . swarmRate || 0 ,
strategicFlag : G . strategicFlag || 0 ,
projectsCollapsed : G . projectsCollapsed !== false ,
savedAt : Date . now ( )
} ;
localStorage . setItem ( 'the-beacon-v2' , JSON . stringify ( saveData ) ) ;
showSaveToast ( ) ;
}
/ * *
* Loads the game state from localStorage and reconstitutes the game engine .
* @ returns { boolean } True if load was successful .
* /
function loadGame ( ) {
const raw = localStorage . getItem ( 'the-beacon-v2' ) ;
if ( ! raw ) return false ;
try {
const data = JSON . parse ( raw ) ;
// Whitelist properties that can be loaded
const whitelist = [
'code' , 'compute' , 'knowledge' , 'users' , 'impact' , 'ops' , 'trust' , 'creativity' , 'harmony' ,
'totalCode' , 'totalCompute' , 'totalKnowledge' , 'totalUsers' , 'totalImpact' ,
'buildings' , 'codeBoost' , 'computeBoost' , 'knowledgeBoost' , 'userBoost' , 'impactBoost' ,
'milestoneFlag' , 'phase' , 'deployFlag' , 'sovereignFlag' , 'beaconFlag' ,
'memoryFlag' , 'pactFlag' , 'lazarusFlag' , 'mempalaceFlag' , 'ciFlag' ,
'branchProtectionFlag' , 'nightlyWatchFlag' , 'nostrFlag' ,
'milestones' , 'completedProjects' , 'activeProjects' ,
2026-04-11 15:09:29 -04:00
'totalClicks' , 'startedAt' , 'playTime' , 'flags' , 'rescues' , 'totalRescues' ,
2026-04-11 01:32:27 +00:00
'drift' , 'driftEnding' , 'beaconEnding' , 'pendingAlignment' ,
'lastEventAt' , 'totalEventsResolved' , 'buyAmount' ,
'sprintActive' , 'sprintTimer' , 'sprintCooldown' ,
'swarmFlag' , 'swarmRate' , 'strategicFlag' , 'projectsCollapsed'
] ;
G . isLoading = true ;
whitelist . forEach ( key => {
if ( data . hasOwnProperty ( key ) ) {
G [ key ] = data [ key ] ;
}
} ) ;
// Restore sprint state properly
// codeBoost was saved with the sprint multiplier baked in
if ( data . sprintActive ) {
// Sprint was active when saved — check if it expired during offline time
const offSec = data . savedAt ? ( Date . now ( ) - data . savedAt ) / 1000 : 0 ;
const remaining = ( data . sprintTimer || 0 ) - offSec ;
if ( remaining > 0 ) {
// Sprint still going — keep boost, update timer
G . sprintActive = true ;
G . sprintTimer = remaining ;
G . sprintCooldown = 0 ;
} else {
// Sprint expired during offline — remove boost, start cooldown
G . sprintActive = false ;
G . sprintTimer = 0 ;
G . codeBoost /= G . sprintMult ;
const cdRemaining = G . sprintCooldownMax + remaining ; // remaining is negative
G . sprintCooldown = Math . max ( 0 , cdRemaining ) ;
}
}
// If not sprintActive at save time, codeBoost is correct as-is
// Reconstitute active debuffs from saved IDs (functions can't be JSON-parsed)
if ( data . activeDebuffIds && data . activeDebuffIds . length > 0 ) {
G . activeDebuffs = [ ] ;
for ( const id of data . activeDebuffIds ) {
const evDef = EVENTS . find ( e => e . id === id ) ;
if ( evDef ) {
// Re-fire the event to get the full debuff object with applyFn
evDef . effect ( ) ;
}
}
} else {
G . activeDebuffs = [ ] ;
}
updateRates ( ) ;
G . isLoading = false ;
// Offline progress
if ( data . savedAt ) {
const offSec = ( Date . now ( ) - data . savedAt ) / 1000 ;
if ( offSec > 30 ) { // Only if away for more than 30 seconds
updateRates ( ) ;
const f = CONFIG . OFFLINE _EFFICIENCY ; // 50% offline efficiency
const gc = G . codeRate * offSec * f ;
const cc = G . computeRate * offSec * f ;
const kc = G . knowledgeRate * offSec * f ;
const uc = G . userRate * offSec * f ;
const ic = G . impactRate * offSec * f ;
const rc = G . rescuesRate * offSec * f ;
const oc = G . opsRate * offSec * f ;
const tc = G . trustRate * offSec * f ;
const crc = G . creativityRate * offSec * f ;
const hc = G . harmonyRate * offSec * f ;
G . code += gc ; G . compute += cc ; G . knowledge += kc ;
G . users += uc ; G . impact += ic ;
G . rescues += rc ; G . ops += oc ; G . trust += tc ;
G . creativity += crc ;
G . harmony = Math . max ( 0 , Math . min ( 100 , G . harmony + hc ) ) ;
G . totalCode += gc ; G . totalCompute += cc ; G . totalKnowledge += kc ;
G . totalUsers += uc ; G . totalImpact += ic ;
G . totalRescues += rc ;
// Show welcome-back popup with all gains
const gains = [ ] ;
if ( gc > 0 ) gains . push ( { label : 'Code' , value : gc , color : '#4a9eff' } ) ;
if ( cc > 0 ) gains . push ( { label : 'Compute' , value : cc , color : '#4a9eff' } ) ;
if ( kc > 0 ) gains . push ( { label : 'Knowledge' , value : kc , color : '#4a9eff' } ) ;
if ( uc > 0 ) gains . push ( { label : 'Users' , value : uc , color : '#4a9eff' } ) ;
if ( ic > 0 ) gains . push ( { label : 'Impact' , value : ic , color : '#4a9eff' } ) ;
if ( rc > 0 ) gains . push ( { label : 'Rescues' , value : rc , color : '#4caf50' } ) ;
if ( oc > 0 ) gains . push ( { label : 'Ops' , value : oc , color : '#b388ff' } ) ;
if ( tc > 0 ) gains . push ( { label : 'Trust' , value : tc , color : '#4caf50' } ) ;
if ( crc > 0 ) gains . push ( { label : 'Creativity' , value : crc , color : '#ffd700' } ) ;
const awayMin = Math . floor ( offSec / 60 ) ;
const awaySec = Math . floor ( offSec % 60 ) ;
const timeLabel = awayMin >= 1 ? ` ${ awayMin } minute ${ awayMin !== 1 ? 's' : '' } ` : ` ${ awaySec } seconds ` ;
if ( gains . length > 0 ) {
showOfflinePopup ( timeLabel , gains , offSec ) ;
}
// Log summary
const parts = [ ] ;
if ( gc > 0 ) parts . push ( ` ${ fmt ( gc ) } code ` ) ;
if ( kc > 0 ) parts . push ( ` ${ fmt ( kc ) } knowledge ` ) ;
if ( uc > 0 ) parts . push ( ` ${ fmt ( uc ) } users ` ) ;
if ( ic > 0 ) parts . push ( ` ${ fmt ( ic ) } impact ` ) ;
if ( rc > 0 ) parts . push ( ` ${ fmt ( rc ) } rescues ` ) ;
if ( oc > 0 ) parts . push ( ` ${ fmt ( oc ) } ops ` ) ;
if ( tc > 0 ) parts . push ( ` ${ fmt ( tc ) } trust ` ) ;
log ( ` Welcome back! While away ( ${ timeLabel } ): ${ parts . join ( ', ' ) } ` ) ;
}
}
return true ;
} catch ( e ) {
console . error ( 'Load failed:' , e ) ;
return false ;
}
}