Replace linear trust thresholds with Fibonacci milestones. fib=[2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597] nextTrust=fib[n]*1000 Each milestone: - Logs a narrative message (educational: exponential growth) - Grants a small permanent bonus (ops rate, trust rate, boosts) - Shows particle burst on trust resource - Progress indicator in trust resource display 15 milestones total. Educational: Fibonacci growth feels natural because it IS natural — spiral shells, sunflower seeds, galaxies. Closes #7
184 lines
5.8 KiB
JavaScript
184 lines
5.8 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
|
|
class Element {
|
|
constructor(tagName = 'div', id = '') {
|
|
this.tagName = String(tagName).toUpperCase();
|
|
this.id = id;
|
|
this.style = {};
|
|
this.children = [];
|
|
this.parentNode = null;
|
|
this.textContent = '';
|
|
this.innerHTML = '';
|
|
this.className = '';
|
|
this.attributes = {};
|
|
}
|
|
appendChild(child) {
|
|
child.parentNode = this;
|
|
this.children.push(child);
|
|
return child;
|
|
}
|
|
removeChild(child) {
|
|
this.children = this.children.filter((c) => c !== child);
|
|
if (child.parentNode === this) child.parentNode = null;
|
|
return child;
|
|
}
|
|
remove() {
|
|
if (this.parentNode) this.parentNode.removeChild(this);
|
|
}
|
|
setAttribute(name, value) {
|
|
this.attributes[name] = value;
|
|
if (name === 'id') this.id = value;
|
|
if (name === 'class') this.className = value;
|
|
}
|
|
closest(selector) {
|
|
if (selector === '.res' && this.className.split(/\s+/).includes('res')) return this;
|
|
return this.parentNode && typeof this.parentNode.closest === 'function'
|
|
? this.parentNode.closest(selector)
|
|
: null;
|
|
}
|
|
querySelectorAll() { return []; }
|
|
querySelector() { return null; }
|
|
getBoundingClientRect() { return { left: 0, top: 0, width: 16, height: 16 }; }
|
|
}
|
|
|
|
function buildDom() {
|
|
const byId = new Map();
|
|
const body = new Element('body', 'body');
|
|
const head = new Element('head', 'head');
|
|
const document = {
|
|
body,
|
|
head,
|
|
createElement(tagName) { return new Element(tagName); },
|
|
getElementById(id) { return byId.get(id) || null; },
|
|
querySelector() { return null; },
|
|
querySelectorAll() { return []; },
|
|
addEventListener() {},
|
|
removeEventListener() {},
|
|
};
|
|
|
|
function register(el) {
|
|
if (el.id) byId.set(el.id, el);
|
|
return el;
|
|
}
|
|
|
|
const trustWrapper = register(new Element('div'));
|
|
trustWrapper.className = 'res';
|
|
const trustValue = register(new Element('div', 'r-trust'));
|
|
trustWrapper.appendChild(trustValue);
|
|
body.appendChild(trustWrapper);
|
|
|
|
const phaseProgress = register(new Element('div', 'phase-progress'));
|
|
const phaseProgressLabel = register(new Element('div', 'phase-progress-label'));
|
|
const phaseProgressTarget = register(new Element('div', 'phase-progress-target'));
|
|
const milestoneChips = register(new Element('div', 'milestone-chips'));
|
|
body.appendChild(phaseProgress);
|
|
body.appendChild(phaseProgressLabel);
|
|
body.appendChild(phaseProgressTarget);
|
|
body.appendChild(milestoneChips);
|
|
|
|
return { document, window: { document, innerWidth: 1280, innerHeight: 720, addEventListener() {}, removeEventListener() {} } };
|
|
}
|
|
|
|
function loadBeacon() {
|
|
const { document, window } = buildDom();
|
|
const storage = new Map();
|
|
const context = {
|
|
console,
|
|
Math,
|
|
Date,
|
|
document,
|
|
window,
|
|
navigator: { userAgent: 'node' },
|
|
requestAnimationFrame: (fn) => fn(),
|
|
setTimeout: (fn) => { fn(); return 1; },
|
|
clearTimeout: () => {},
|
|
localStorage: {
|
|
getItem: (key) => (storage.has(key) ? storage.get(key) : null),
|
|
setItem: (key, value) => storage.set(key, String(value)),
|
|
removeItem: (key) => storage.delete(key),
|
|
},
|
|
Combat: { tickBattle() {}, renderCombatPanel() {} },
|
|
Sound: { playMilestone() {} },
|
|
showToast() {},
|
|
log() {},
|
|
showOfflinePopup() {},
|
|
location: { reload() {} },
|
|
confirm: () => false,
|
|
};
|
|
|
|
vm.createContext(context);
|
|
const source = ['js/data.js', 'js/utils.js', 'js/engine.js', 'js/render.js']
|
|
.map((file) => fs.readFileSync(path.join(ROOT, file), 'utf8'))
|
|
.join('\n\n');
|
|
|
|
vm.runInContext(`${source}
|
|
this.__exports = {
|
|
G,
|
|
updateRates,
|
|
saveGame,
|
|
loadGame,
|
|
renderTrustMilestone: typeof renderTrustMilestone === 'function' ? renderTrustMilestone : null,
|
|
checkTrustMilestones: typeof checkTrustMilestones === 'function' ? checkTrustMilestones : null,
|
|
TRUST_MILESTONES: typeof TRUST_MILESTONES !== 'undefined' ? TRUST_MILESTONES : null
|
|
};`, context);
|
|
|
|
return { ...context.__exports, document };
|
|
}
|
|
|
|
test('trust milestones use Fibonacci thresholds', () => {
|
|
const { TRUST_MILESTONES } = loadBeacon();
|
|
assert.ok(Array.isArray(TRUST_MILESTONES), 'TRUST_MILESTONES should exist');
|
|
assert.deepEqual(
|
|
Array.from(TRUST_MILESTONES.slice(0, 5).map((m) => m.threshold)),
|
|
[2000, 3000, 5000, 8000, 13000]
|
|
);
|
|
});
|
|
|
|
test('trust milestone bonuses persist after updateRates recalculation', () => {
|
|
const { G, updateRates, checkTrustMilestones } = loadBeacon();
|
|
assert.equal(typeof checkTrustMilestones, 'function', 'checkTrustMilestones should exist');
|
|
|
|
G.trust = 2000;
|
|
G.trustMilestoneIndex = 0;
|
|
checkTrustMilestones();
|
|
updateRates();
|
|
|
|
assert.ok(G.opsRate >= 1, 'first Fibonacci trust milestone should grant persistent ops bonus');
|
|
});
|
|
|
|
test('renderTrustMilestone shows next Fibonacci trust target and percent', () => {
|
|
const { G, renderTrustMilestone, document } = loadBeacon();
|
|
assert.equal(typeof renderTrustMilestone, 'function', 'renderTrustMilestone should exist');
|
|
|
|
G.trust = 2500;
|
|
G.trustMilestoneIndex = 1; // next milestone = 3000
|
|
renderTrustMilestone();
|
|
|
|
const display = document.body.children
|
|
.flatMap((child) => child.children || [])
|
|
.find((child) => child.id === 'trust-milestone-display');
|
|
assert.ok(display, 'should render a trust milestone display element');
|
|
assert.match(display.textContent || display.innerHTML, /3,000|3000|3\.0K/);
|
|
assert.match(display.textContent || display.innerHTML, /50%/);
|
|
});
|
|
|
|
test('save and load preserve trust milestone progress', () => {
|
|
const { G, saveGame, loadGame } = loadBeacon();
|
|
G.startedAt = Date.now();
|
|
G.trustMilestoneIndex = 4;
|
|
G.trust = 14000;
|
|
saveGame();
|
|
|
|
G.trustMilestoneIndex = 0;
|
|
G.trust = 0;
|
|
assert.equal(loadGame(), true);
|
|
assert.equal(G.trustMilestoneIndex, 4);
|
|
assert.equal(G.trust, 14000);
|
|
});
|