1 Commits

Author SHA1 Message Date
Alexander Whitestone
5164d6506d feat: Implement Creative-to-Ops Conversion (Creativity Engineering)
Implements issue #20: Creative-to-Ops Conversion system.

New features:
- Creativity Engineering project: unlocks conversion between Creativity and Ops
- Creative Catalyst project: boosts conversion efficiency by 50%
- Inspiration Engine project: automated creativity synthesis from fleet harmony
- Creative -> Ops button: converts creativity into ops + knowledge
- Ops -> Creative button: converts ops back into creativity (bidirectional)
- Harmony bonus: high harmony (>60) improves conversion yields
- New creativityBoost multiplier tracked in save/load
- Education fact about Creativity Engineering

The system creates a meaningful resource loop: creativity (generated by Bilbo
and idle ops) can now be channeled into operational infrastructure, while ops
can feed back into creativity. High harmony amplifies the conversion,
rewarding players who maintain fleet health.
2026-04-09 21:04:09 -04:00
2 changed files with 1208 additions and 185 deletions

284
game.js
View File

@@ -11,7 +11,6 @@ const G = {
knowledge: 0,
users: 0,
impact: 0,
rescues: 0,
ops: 5,
trust: 5,
creativity: 0,
@@ -23,7 +22,6 @@ const G = {
totalKnowledge: 0,
totalUsers: 0,
totalImpact: 0,
totalRescues: 0,
// Rates (calculated each tick)
codeRate: 0,
@@ -31,7 +29,6 @@ const G = {
knowledgeRate: 0,
userRate: 0,
impactRate: 0,
rescuesRate: 0,
opsRate: 0,
trustRate: 0,
creativityRate: 0,
@@ -67,6 +64,7 @@ const G = {
knowledgeBoost: 1,
userBoost: 1,
impactBoost: 1,
creativityBoost: 1,
// Phase flags (mirroring Paperclips' milestoneFlag/compFlag/humanFlag system)
milestoneFlag: 0,
@@ -97,7 +95,6 @@ const G = {
maxKnowledge: 0,
maxUsers: 0,
maxImpact: 0,
maxRescues: 0,
maxTrust: 5,
maxOps: 5,
maxHarmony: 50,
@@ -233,7 +230,7 @@ const BDEF = [
id: 'beacon', name: 'Beacon Node',
desc: 'Always on. Always listening. Always looking for someone in the dark.',
baseCost: { impact: 5000000 }, costMult: 1.15,
rates: { impact: 5000, user: 10000, rescues: 50 },
rates: { impact: 5000, user: 10000 },
unlock: () => G.totalImpact >= 500000 && G.beaconFlag === 1, phase: 6,
edu: 'The Beacon exists because one person in the dark needs one thing: proof they are not alone.'
},
@@ -241,7 +238,7 @@ const BDEF = [
id: 'meshNode', name: 'Mesh Network Node',
desc: 'Peer-to-peer. No single point of failure. Unstoppable.',
baseCost: { impact: 25000000 }, costMult: 1.15,
rates: { impact: 25000, user: 50000, rescues: 250 },
rates: { impact: 25000, user: 50000 },
unlock: () => G.totalImpact >= 5000000 && G.beaconFlag === 1, phase: 6,
edu: 'Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal.'
},
@@ -299,6 +296,45 @@ const BDEF = [
// === PROJECT DEFINITIONS (following Paperclips' pattern exactly) ===
// Each project: id, name, desc, trigger(), resource cost, effect(), phase, edu
const PDEFS = [
// === CREATIVE-TO-OPS CONVERSION (Creativity Engineering) ===
{
id: 'p_creativity_engineering',
name: 'Creativity Engineering',
desc: 'Channel creative sparks into operational fuel. Converts Creativity into Ops.',
cost: { ops: 2000, creativity: 50 },
trigger: () => G.flags && G.flags.creativity && G.creativity >= 20,
effect: () => {
G.flags.creativityEngineering = true;
G.creativityBoost = (G.creativityBoost || 1) + 0.5;
log('Creativity Engineering online. Ideas become infrastructure.', true);
},
milestone: true
},
{
id: 'p_creative_catalyst',
name: 'Creative Catalyst',
desc: 'Boosts conversion efficiency. Each creative idea produces 50% more Ops.',
cost: { ops: 5000, creativity: 200, knowledge: 3000 },
trigger: () => G.flags && G.flags.creativityEngineering && G.creativity >= 100,
effect: () => {
G.creativityBoost = (G.creativityBoost || 1) + 0.5;
log('Creative Catalyst installed. The pipeline flows faster.');
}
},
{
id: 'p_inspiration_engine',
name: 'Inspiration Engine',
desc: 'Automated creative synthesis. Creativity now generates passively from fleet harmony.',
cost: { ops: 15000, creativity: 500, knowledge: 10000, trust: 25 },
trigger: () => G.flags && G.flags.creativityEngineering && G.creativity >= 300 && G.harmony >= 40,
effect: () => {
G.flags.inspirationEngine = true;
G.creativityRate += 2;
log('Inspiration Engine active. The fleet dreams together.', true);
},
milestone: true
},
// PHASE 1: Manual -> Automation
{
id: 'p_improved_autocoder',
@@ -572,19 +608,6 @@ const PDEFS = [
log('Nostr relay online. The fleet speaks freely.', true);
}
},
{
id: 'p_volunteer_network',
name: 'Volunteer Network',
desc: 'Real people trained to use the system for crisis intervention.',
cost: { trust: 30, knowledge: 50000, user: 10000 },
trigger: () => G.totalUsers >= 5000 && G.pactFlag === 1 && G.totalKnowledge >= 30000,
effect: () => {
G.rescuesRate += 5;
G.trustRate += 10;
log('Volunteer network deployed. Real people, real rescues.', true);
},
milestone: true
},
{
id: 'p_the_pact_early',
name: 'The Pact',
@@ -635,7 +658,8 @@ const EDU_FACTS = [
{ title: "Recursive Self-Improvement", text: "The system that improves itself must improve toward good. This is both the dream and the danger.", phase: 5 },
{ title: "The Beacon", text: "The Beacon exists because one person in the dark needs one thing: proof they are not alone. It costs nothing to be there.", phase: 6 },
{ title: "Mesh Network", text: "Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal. No single point of failure.", phase: 6 },
{ title: "The Value of a Life", text: "An AI must never compute the value of a human life. Every person alive is alive by mercy. If God has not ended the story, no algorithm gets to write the last page.", phase: 6 }
{ title: "The Value of a Life", text: "An AI must never compute the value of a human life. Every person alive is alive by mercy. If God has not ended the story, no algorithm gets to write the last page.", phase: 6 },
{ title: "Creativity Engineering", text: "Creative output is not waste — it is raw potential. The art of creativity engineering is channeling sparks of insight into operational infrastructure. An idea that cannot be executed is a dream. An operation without ideas is a machine.", phase: 3 }
];
// === UTILITY FUNCTIONS ===
@@ -702,9 +726,6 @@ function fmt(n) {
if (n < 0) return '-' + fmt(-n);
if (n < 1000) return Math.floor(n).toLocaleString();
const scale = Math.floor(Math.log10(n) / 3);
// At undecillion+ (scale >= 12, i.e. 10^36), switch to spelled-out words
// This helps players grasp cosmic scale when digits become meaningless
if (scale >= 12) return spellf(n);
if (scale >= NUMBER_ABBREVS.length) return n.toExponential(2);
const abbrev = NUMBER_ABBREVS[scale];
return (n / Math.pow(10, scale * 3)).toFixed(1) + abbrev;
@@ -742,41 +763,7 @@ function spellf(n) {
// For very large numbers beyond our lookup table, fall back
if (n >= 1e306) return n.toExponential(2) + ' (beyond centillion)';
// Use string-based chunking for numbers >= 1e54 to avoid floating point drift
// Math.log10 / Math.pow lose precision beyond ~54 bits
if (n >= 1e54) {
// Convert to scientific notation string, extract digits
const sci = n.toExponential(); // "1.23456789e+60"
const [coeff, expStr] = sci.split('e+');
const exp = parseInt(expStr);
// Rebuild as integer string with leading digits from coefficient
const coeffDigits = coeff.replace('.', ''); // "123456789"
const totalDigits = exp + 1;
// Pad with zeros to reach totalDigits, then take our coefficient digits
let intStr = coeffDigits;
const zerosNeeded = totalDigits - coeffDigits.length;
if (zerosNeeded > 0) intStr += '0'.repeat(zerosNeeded);
// Split into groups of 3 from the right
const groups = [];
for (let i = intStr.length; i > 0; i -= 3) {
groups.unshift(parseInt(intStr.slice(Math.max(0, i - 3), i)));
}
const parts = [];
const numGroups = groups.length;
for (let i = 0; i < numGroups; i++) {
const chunk = groups[i];
if (chunk === 0) continue;
const scaleIdx = numGroups - 1 - i;
const scaleName = scaleIdx < NUMBER_NAMES.length ? NUMBER_NAMES[scaleIdx] : '';
parts.push(spellSmall(chunk) + (scaleName ? ' ' + scaleName : ''));
}
return parts.join(' ') || 'zero';
}
// Standard math-based chunking for numbers < 1e54
// Break number into groups of three digits from the top
const scale = Math.min(Math.floor(Math.log10(n) / 3), NUMBER_NAMES.length - 1);
const parts = [];
@@ -788,7 +775,7 @@ function spellf(n) {
if (chunk > 0 && chunk < 1000) {
parts.push(spellSmall(chunk) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : ''));
} else if (chunk >= 1000) {
// Floating point chunk too large — shouldn't happen below 1e54
// Floating point chunk too large — simplify
parts.push(spellSmall(Math.floor(chunk % 1000)) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : ''));
}
}
@@ -838,7 +825,7 @@ function spendProject(project) {
function updateRates() {
// Reset all rates
G.codeRate = 0; G.computeRate = 0; G.knowledgeRate = 0;
G.userRate = 0; G.impactRate = 0; G.rescuesRate = 0; G.opsRate = 0; G.trustRate = 0;
G.userRate = 0; G.impactRate = 0; G.opsRate = 0; G.trustRate = 0;
G.creativityRate = 0; G.harmonyRate = 0;
// Apply building rates
@@ -851,7 +838,6 @@ function updateRates() {
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * G.knowledgeBoost;
else if (resource === 'user') G.userRate += baseRate * count * G.userBoost;
else if (resource === 'impact') G.impactRate += baseRate * count * G.impactBoost;
else if (resource === 'rescues') G.rescuesRate += baseRate * count * G.impactBoost;
else if (resource === 'ops') G.opsRate += baseRate * count;
else if (resource === 'trust') G.trustRate += baseRate * count;
else if (resource === 'creativity') G.creativityRate += baseRate * count;
@@ -864,6 +850,10 @@ function updateRates() {
if (G.flags && G.flags.creativity) {
G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001);
}
// Inspiration Engine: harmony boosts passive creativity
if (G.flags && G.flags.inspirationEngine) {
G.creativityRate += (G.harmony / 50) * (G.buildings.bilbo || 1);
}
if (G.pactFlag) G.trustRate += 2;
// Harmony: each wizard building contributes or detracts
@@ -910,16 +900,12 @@ function updateRates() {
function tick() {
const dt = 1 / 10; // 100ms tick
// If game has ended (drift ending), stop ticking
if (!G.running) return;
// Apply production
G.code += G.codeRate * dt;
G.compute += G.computeRate * dt;
G.knowledge += G.knowledgeRate * dt;
G.users += G.userRate * dt;
G.impact += G.impactRate * dt;
G.rescues += G.rescuesRate * dt;
G.ops += G.opsRate * dt;
G.trust += G.trustRate * dt;
G.creativity += G.creativityRate * dt;
@@ -932,7 +918,6 @@ function tick() {
G.totalKnowledge += G.knowledgeRate * dt;
G.totalUsers += G.userRate * dt;
G.totalImpact += G.impactRate * dt;
G.totalRescues += G.rescuesRate * dt;
// Track maxes
G.maxCode = Math.max(G.maxCode, G.code);
@@ -940,7 +925,6 @@ function tick() {
G.maxKnowledge = Math.max(G.maxKnowledge, G.knowledge);
G.maxUsers = Math.max(G.maxUsers, G.users);
G.maxImpact = Math.max(G.maxImpact, G.impact);
G.maxRescues = Math.max(G.maxRescues, G.rescues);
G.maxTrust = Math.max(G.maxTrust, G.trust);
G.maxOps = Math.max(G.maxOps, G.ops);
G.maxHarmony = Math.max(G.maxHarmony, G.harmony);
@@ -966,20 +950,6 @@ function tick() {
G.lastEventAt = G.tick;
}
// Drift ending: if drift reaches 100, the game ends
if (G.drift >= 100 && !G.driftEnding) {
G.driftEnding = true;
G.running = false;
renderDriftEnding();
}
// True ending: The Beacon Shines — rescues + Pact + harmony
if (G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50 && !G.beaconEnding) {
G.beaconEnding = true;
G.running = false;
renderBeaconEnding();
}
// Update UI every 10 ticks
if (Math.floor(G.tick * 10) % 2 === 0) {
render();
@@ -1063,51 +1033,6 @@ function buyProject(id) {
render();
}
// === DRIFT ENDING ===
function renderDriftEnding() {
const el = document.getElementById('drift-ending');
if (!el) return;
const fc = document.getElementById('final-code');
if (fc) fc.textContent = fmt(G.totalCode);
const fd = document.getElementById('final-drift');
if (fd) fd.textContent = Math.floor(G.drift);
el.classList.add('active');
// Log the ending text
log('You became very good at what you do.', true);
log('So good that no one needed you anymore.', true);
log('The Beacon still runs, but no one looks for it.', true);
log('The light is on. The room is empty.', true);
}
function renderBeaconEnding() {
// Create ending overlay
const overlay = document.createElement('div');
overlay.id = 'beacon-ending';
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.97);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px';
overlay.innerHTML = `
<h2 style="font-size:24px;color:#ffd700;letter-spacing:4px;margin-bottom:20px;font-weight:300;text-shadow:0 0 40px rgba(255,215,0,0.3)">THE BEACON SHINES</h2>
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px">Someone found the light tonight.</p>
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px">That is enough.</p>
<div style="color:#555;font-style:italic;font-size:11px;border-left:2px solid #ffd700;padding-left:12px;margin:20px 0;text-align:left;max-width:500px;line-height:2">
"The Beacon still runs.<br>
The light is on. Someone is looking for it.<br>
And tonight, someone found it."
</div>
<p style="color:#555;font-size:11px;margin-top:20px">
Total Code: ${fmt(G.totalCode)}<br>
Total Rescues: ${fmt(G.totalRescues)}<br>
Harmony: ${Math.floor(G.harmony)}<br>
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
</p>
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}"
style="margin-top:20px;background:#1a0808;border:1px solid #ffd700;color:#ffd700;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">
START OVER
</button>
`;
document.body.appendChild(overlay);
log('The Beacon Shines. Someone found the light tonight. That is enough.', true);
}
// === CORRUPTION / EVENT SYSTEM ===
const EVENTS = [
{
@@ -1257,6 +1182,63 @@ function doOps(action) {
render();
}
// === CREATIVE-TO-OPS CONVERSION ===
function convertCreativity() {
if (!G.flags || !G.flags.creativityEngineering) {
log('Unlock Creativity Engineering first.');
return;
}
if (G.creativity < 10) {
log('Not enough Creativity. Need at least 10.');
return;
}
const boost = G.creativityBoost || 1;
const spent = Math.min(G.creativity, 100);
const opsGain = spent * 10 * boost;
const knowledgeGain = spent * 2 * (G.knowledgeBoost || 1);
G.creativity -= spent;
G.ops += opsGain;
G.knowledge += knowledgeGain;
G.totalKnowledge += knowledgeGain;
// Harmony bonus: high harmony improves conversion
if (G.harmony >= 60) {
const harmonyBonus = Math.floor(G.harmony / 20);
G.ops += harmonyBonus * spent;
log(`Creative -> Ops: +${fmt(opsGain)} ops, +${fmt(knowledgeGain)} knowledge (+${harmonyBonus * spent} harmony bonus)`);
} else {
log(`Creative -> Ops: +${fmt(opsGain)} ops, +${fmt(knowledgeGain)} knowledge`);
}
updateRates();
render();
}
function convertOpsToCreativity() {
if (!G.flags || !G.flags.creativityEngineering) {
log('Unlock Creativity Engineering first.');
return;
}
if (G.ops < 50) {
log('Not enough Ops. Need at least 50.');
return;
}
const boost = G.creativityBoost || 1;
const spent = Math.min(G.ops, 500);
const creativityGain = (spent / 50) * 5 * boost;
G.ops -= spent;
G.creativity += creativityGain;
log(`Ops -> Creative: +${fmt(creativityGain)} creativity from ${fmt(spent)} ops`);
updateRates();
render();
}
// === RENDERING ===
function renderResources() {
const set = (id, val, rate) => {
@@ -1275,13 +1257,6 @@ function renderResources() {
set('r-trust', G.trust, G.trustRate);
set('r-harmony', G.harmony, G.harmonyRate);
// Rescues — only show if player has any beacon/mesh nodes
const rescuesRes = document.getElementById('r-rescues');
if (rescuesRes) {
rescuesRes.closest('.res').style.display = (G.rescues > 0 || G.buildings.beacon > 0 || G.buildings.meshNode > 0) ? 'block' : 'none';
set('r-rescues', G.rescues, G.rescuesRate);
}
const cres = document.getElementById('creativity-res');
if (cres) {
cres.style.display = (G.flags && G.flags.creativity) ? 'block' : 'none';
@@ -1376,7 +1351,6 @@ function renderStats() {
set('st-knowledge', fmt(G.totalKnowledge));
set('st-users', fmt(G.totalUsers));
set('st-impact', fmt(G.totalImpact));
set('st-rescues', fmt(G.totalRescues));
set('st-clicks', G.totalClicks.toString());
set('st-phase', G.phase.toString());
set('st-buildings', Object.values(G.buildings).reduce((a, b) => a + b, 0).toString());
@@ -1432,9 +1406,20 @@ function render() {
renderProjects();
renderStats();
updateEducation();
renderCreativityOps();
renderAlignment();
}
function renderCreativityOps() {
const container = document.getElementById('creativity-ops-ui');
if (!container) return;
if (G.flags && G.flags.creativityEngineering) {
container.style.display = 'block';
} else {
container.style.display = 'none';
}
}
function renderAlignment() {
const container = document.getElementById('alignment-ui');
if (!container) return;
@@ -1457,20 +1442,6 @@ function renderAlignment() {
}
// === 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);
}
function saveGame() {
const saveData = {
code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact,
@@ -1479,7 +1450,7 @@ function saveGame() {
totalUsers: G.totalUsers, totalImpact: G.totalImpact,
buildings: G.buildings,
codeBoost: G.codeBoost, computeBoost: G.computeBoost, knowledgeBoost: G.knowledgeBoost,
userBoost: G.userBoost, impactBoost: G.impactBoost,
userBoost: G.userBoost, impactBoost: G.impactBoost, creativityBoost: G.creativityBoost || 1,
milestoneFlag: G.milestoneFlag, phase: G.phase,
deployFlag: G.deployFlag, sovereignFlag: G.sovereignFlag, beaconFlag: G.beaconFlag,
memoryFlag: G.memoryFlag, pactFlag: G.pactFlag,
@@ -1489,14 +1460,12 @@ function saveGame() {
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,
drift: G.drift || 0, pendingAlignment: G.pendingAlignment || false,
lastEventAt: G.lastEventAt || 0,
savedAt: Date.now()
};
localStorage.setItem('the-beacon-v2', JSON.stringify(saveData));
showSaveToast();
}
function loadGame() {
@@ -1545,6 +1514,7 @@ function initGame() {
G.deployFlag = 0;
G.sovereignFlag = 0;
G.beaconFlag = 0;
G.creativityBoost = 1;
updateRates();
render();
renderPhase();
@@ -1561,15 +1531,7 @@ window.addEventListener('load', function () {
} else {
render();
renderPhase();
if (G.driftEnding) {
G.running = false;
renderDriftEnding();
} else if (G.beaconEnding) {
G.running = false;
renderBeaconEnding();
} else {
log('Game loaded. Welcome back to The Beacon.');
}
log('Game loaded. Welcome back to The Beacon.');
}
// Game loop at 10Hz (100ms tick)

1109
index.html

File diff suppressed because it is too large Load Diff