Compare commits
13 Commits
rescue/exp
...
feat/bette
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0b894a2b6 | ||
|
|
bea958f723 | ||
|
|
b98cf38992 | ||
|
|
7cd47d1159 | ||
|
|
a9e4889d88 | ||
|
|
970f3be00f | ||
|
|
302f6c844d | ||
| 26879de76e | |||
| c197fabc69 | |||
| 9733b9022e | |||
| 967025fbd4 | |||
|
|
9854501bbd | ||
| be0264fc95 |
27
.gitea/workflows/a11y.yml
Normal file
27
.gitea/workflows/a11y.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Accessibility Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
a11y-audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate ARIA Attributes in game.js
|
||||
run: |
|
||||
echo "Checking game.js for ARIA attributes..."
|
||||
grep -q "aria-label" game.js || (echo "ERROR: aria-label missing from game.js" && exit 1)
|
||||
grep -q "aria-valuenow" game.js || (echo "ERROR: aria-valuenow missing from game.js" && exit 1)
|
||||
grep -q "aria-pressed" game.js || (echo "ERROR: aria-pressed missing from game.js" && exit 1)
|
||||
|
||||
- name: Validate ARIA Roles in index.html
|
||||
run: |
|
||||
echo "Checking index.html for ARIA roles..."
|
||||
grep -q "role=" index.html || (echo "ERROR: No ARIA roles found in index.html" && exit 1)
|
||||
|
||||
- name: Syntax Check JS
|
||||
run: |
|
||||
node -c game.js
|
||||
24
.gitea/workflows/smoke.yml
Normal file
24
.gitea/workflows/smoke.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Smoke Test
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Parse check
|
||||
run: |
|
||||
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
|
||||
find . -name '*.json' | xargs -r python3 -m json.tool > /dev/null
|
||||
find . -name '*.py' | xargs -r python3 -m py_compile
|
||||
find . -name '*.sh' | xargs -r bash -n
|
||||
echo "PASS: All files parse"
|
||||
- name: Secret scan
|
||||
run: |
|
||||
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea; then exit 1; fi
|
||||
echo "PASS: No secrets"
|
||||
431
game.js
431
game.js
@@ -4,6 +4,33 @@
|
||||
// ============================================================
|
||||
|
||||
// === GLOBALS (mirroring Paperclips' globals.js pattern) ===
|
||||
const CONFIG = {
|
||||
HARMONY_DRAIN_PER_WIZARD: 0.05,
|
||||
PACT_HARMONY_GAIN: 0.2,
|
||||
WATCH_HARMONY_GAIN: 0.1,
|
||||
MEM_PALACE_HARMONY_GAIN: 0.15,
|
||||
BILBO_BURST_CHANCE: 0.1,
|
||||
BILBO_VANISH_CHANCE: 0.05,
|
||||
EVENT_PROBABILITY: 0.02,
|
||||
OFFLINE_EFFICIENCY: 0.5,
|
||||
AUTO_SAVE_INTERVAL: 30000,
|
||||
COMBO_DECAY: 2.0,
|
||||
SPRINT_COOLDOWN: 60,
|
||||
SPRINT_DURATION: 10,
|
||||
SPRINT_MULTIPLIER: 10,
|
||||
PHASE_2_THRESHOLD: 2000,
|
||||
PHASE_3_THRESHOLD: 20000,
|
||||
PHASE_4_THRESHOLD: 200000,
|
||||
PHASE_5_THRESHOLD: 2000000,
|
||||
PHASE_6_THRESHOLD: 20000000,
|
||||
OPS_RATE_USER_MULT: 0.01,
|
||||
CREATIVITY_RATE_BASE: 0.5,
|
||||
CREATIVITY_RATE_USER_MULT: 0.001,
|
||||
OPS_OVERFLOW_THRESHOLD: 0.8,
|
||||
OPS_OVERFLOW_DRAIN_RATE: 2,
|
||||
OPS_OVERFLOW_CODE_MULT: 10
|
||||
};
|
||||
|
||||
const G = {
|
||||
// Primary resources
|
||||
code: 0,
|
||||
@@ -114,7 +141,7 @@ const G = {
|
||||
// Combo system
|
||||
comboCount: 0,
|
||||
comboTimer: 0,
|
||||
comboDecay: 2.0, // seconds before combo resets
|
||||
comboDecay: CONFIG.COMBO_DECAY, // seconds before combo resets
|
||||
|
||||
// Bulk buy multiplier (1, 10, or -1 for max)
|
||||
buyAmount: 1,
|
||||
@@ -123,23 +150,24 @@ const G = {
|
||||
sprintActive: false,
|
||||
sprintTimer: 0, // seconds remaining on active sprint
|
||||
sprintCooldown: 0, // seconds until sprint available again
|
||||
sprintDuration: 10, // seconds of boost
|
||||
sprintCooldownMax: 60,// seconds cooldown
|
||||
sprintMult: 10, // code multiplier during sprint
|
||||
sprintDuration: CONFIG.SPRINT_DURATION, // seconds of boost
|
||||
sprintCooldownMax: CONFIG.SPRINT_COOLDOWN,// seconds cooldown
|
||||
sprintMult: CONFIG.SPRINT_MULTIPLIER, // code multiplier during sprint
|
||||
|
||||
// Time tracking
|
||||
playTime: 0,
|
||||
startTime: 0
|
||||
startTime: 0,
|
||||
flags: {}
|
||||
};
|
||||
|
||||
// === PHASE DEFINITIONS ===
|
||||
const PHASES = {
|
||||
1: { name: "THE FIRST LINE", threshold: 0, desc: "Write code. Automate. Build the foundation." },
|
||||
2: { name: "LOCAL INFERENCE", threshold: 2000, desc: "You have compute. A model is forming." },
|
||||
3: { name: "DEPLOYMENT", threshold: 20000, desc: "Your AI is live. Users are finding it." },
|
||||
4: { name: "THE NETWORK", threshold: 200000, desc: "Community contributes. The system scales." },
|
||||
5: { name: "SOVEREIGN INTELLIGENCE", threshold: 2000000, desc: "The AI improves itself. You guide, do not control." },
|
||||
6: { name: "THE BEACON", threshold: 20000000, desc: "Always on. Always free. Always looking for someone in the dark." }
|
||||
2: { name: "LOCAL INFERENCE", threshold: CONFIG.PHASE_2_THRESHOLD, desc: "You have compute. A model is forming." },
|
||||
3: { name: "DEPLOYMENT", threshold: CONFIG.PHASE_3_THRESHOLD, desc: "Your AI is live. Users are finding it." },
|
||||
4: { name: "THE NETWORK", threshold: CONFIG.PHASE_4_THRESHOLD, desc: "Community contributes. The system scales." },
|
||||
5: { name: "SOVEREIGN INTELLIGENCE", threshold: CONFIG.PHASE_5_THRESHOLD, desc: "The AI improves itself. You guide, do not control." },
|
||||
6: { name: "THE BEACON", threshold: CONFIG.PHASE_6_THRESHOLD, desc: "Always on. Always free. Always looking for someone in the dark." }
|
||||
};
|
||||
|
||||
// === BUILDING DEFINITIONS ===
|
||||
@@ -362,7 +390,7 @@ const PDEFS = [
|
||||
name: 'Deploy the System',
|
||||
desc: 'Take it live. Let real people use it. No going back.',
|
||||
cost: { trust: 5, compute: 500 },
|
||||
trigger: () => G.totalCode >= 200 && G.totalCompute >= 100,
|
||||
trigger: () => G.totalCode >= 200 && G.totalCompute >= 100 && G.deployFlag === 0,
|
||||
effect: () => {
|
||||
G.deployFlag = 1;
|
||||
G.phase = Math.max(G.phase, 3);
|
||||
@@ -700,6 +728,7 @@ const EDU_FACTS = [
|
||||
|
||||
// === TOAST NOTIFICATIONS ===
|
||||
function showToast(msg, type = 'info', duration = 4000) {
|
||||
if (G.isLoading) return;
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
const toast = document.createElement('div');
|
||||
@@ -772,6 +801,11 @@ const NUMBER_NAMES = [
|
||||
'octononagintillion', 'novemnonagintillion', 'centillion' // 10^297, 10^300, 10^303
|
||||
];
|
||||
|
||||
/**
|
||||
* Formats a number into a readable string with abbreviations.
|
||||
* @param {number} n - The number to format.
|
||||
* @returns {string} The formatted string.
|
||||
*/
|
||||
function fmt(n) {
|
||||
if (n === undefined || n === null || isNaN(n)) return '0';
|
||||
if (n === Infinity) return '\u221E';
|
||||
@@ -800,6 +834,11 @@ function getScaleName(n) {
|
||||
// Examples: spellf(1500) => "one thousand five hundred"
|
||||
// spellf(2500000) => "two million five hundred thousand"
|
||||
// spellf(1e33) => "one decillion"
|
||||
/**
|
||||
* Formats a number into a full word string (e.g., "1.5 million").
|
||||
* @param {number} n - The number to format.
|
||||
* @returns {string} The formatted string.
|
||||
*/
|
||||
function spellf(n) {
|
||||
if (n === undefined || n === null || isNaN(n)) return 'zero';
|
||||
if (n === Infinity) return 'infinity';
|
||||
@@ -965,23 +1004,36 @@ function spendProject(project) {
|
||||
}
|
||||
}
|
||||
|
||||
function getClickPower() {
|
||||
return (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates production rates for all resources based on buildings and boosts.
|
||||
*/
|
||||
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.creativityRate = 0; G.harmonyRate = 0;
|
||||
|
||||
// Apply building rates
|
||||
// Snapshot base boosts BEFORE debuffs modify them
|
||||
// Without this, debuffs permanently degrade boost multipliers on each updateRates() call
|
||||
let _codeBoost = G.codeBoost, _computeBoost = G.computeBoost;
|
||||
let _knowledgeBoost = G.knowledgeBoost, _userBoost = G.userBoost;
|
||||
let _impactBoost = G.impactBoost;
|
||||
|
||||
// Apply building rates using snapshot boosts (immune to debuff mutation)
|
||||
for (const def of BDEF) {
|
||||
const count = G.buildings[def.id] || 0;
|
||||
if (count > 0 && def.rates) {
|
||||
for (const [resource, baseRate] of Object.entries(def.rates)) {
|
||||
if (resource === 'code') G.codeRate += baseRate * count * G.codeBoost;
|
||||
else if (resource === 'compute') G.computeRate += baseRate * count * G.computeBoost;
|
||||
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;
|
||||
if (resource === 'code') G.codeRate += baseRate * count * _codeBoost;
|
||||
else if (resource === 'compute') G.computeRate += baseRate * count * _computeBoost;
|
||||
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * _knowledgeBoost;
|
||||
else if (resource === 'user') G.userRate += baseRate * count * _userBoost;
|
||||
else if (resource === 'impact') G.impactRate += baseRate * count * _impactBoost;
|
||||
else if (resource === 'rescues') G.rescuesRate += baseRate * count * _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;
|
||||
@@ -990,9 +1042,9 @@ function updateRates() {
|
||||
}
|
||||
|
||||
// Passive generation
|
||||
G.opsRate += Math.max(1, G.totalUsers * 0.01);
|
||||
G.opsRate += Math.max(1, G.totalUsers * CONFIG.OPS_RATE_USER_MULT);
|
||||
if (G.flags && G.flags.creativity) {
|
||||
G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001);
|
||||
G.creativityRate += CONFIG.CREATIVITY_RATE_BASE + Math.max(0, G.totalUsers * CONFIG.CREATIVITY_RATE_USER_MULT);
|
||||
}
|
||||
if (G.pactFlag) G.trustRate += 2;
|
||||
|
||||
@@ -1003,24 +1055,24 @@ function updateRates() {
|
||||
G.harmonyBreakdown = [];
|
||||
if (wizardCount > 0) {
|
||||
// Baseline harmony drain from complexity
|
||||
const drain = -0.05 * wizardCount;
|
||||
const drain = -CONFIG.HARMONY_DRAIN_PER_WIZARD * wizardCount;
|
||||
G.harmonyRate = drain;
|
||||
G.harmonyBreakdown.push({ label: `${wizardCount} wizards`, value: drain });
|
||||
// The Pact restores harmony
|
||||
if (G.pactFlag) {
|
||||
const pact = 0.2 * wizardCount;
|
||||
const pact = CONFIG.PACT_HARMONY_GAIN * wizardCount;
|
||||
G.harmonyRate += pact;
|
||||
G.harmonyBreakdown.push({ label: 'The Pact', value: pact });
|
||||
}
|
||||
// Nightly Watch restores harmony
|
||||
if (G.nightlyWatchFlag) {
|
||||
const watch = 0.1 * wizardCount;
|
||||
const watch = CONFIG.WATCH_HARMONY_GAIN * wizardCount;
|
||||
G.harmonyRate += watch;
|
||||
G.harmonyBreakdown.push({ label: 'Nightly Watch', value: watch });
|
||||
}
|
||||
// MemPalace restores harmony
|
||||
if (G.mempalaceFlag) {
|
||||
const mem = 0.15 * wizardCount;
|
||||
const mem = CONFIG.MEM_PALACE_HARMONY_GAIN * wizardCount;
|
||||
G.harmonyRate += mem;
|
||||
G.harmonyBreakdown.push({ label: 'MemPalace', value: mem });
|
||||
}
|
||||
@@ -1045,11 +1097,11 @@ function updateRates() {
|
||||
}
|
||||
|
||||
// Bilbo randomness: 10% chance of massive creative burst
|
||||
if (G.buildings.bilbo > 0 && Math.random() < 0.1) {
|
||||
if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_BURST_CHANCE) {
|
||||
G.creativityRate += 50 * G.buildings.bilbo;
|
||||
}
|
||||
// Bilbo vanishing: 5% chance of zero creativity this tick
|
||||
if (G.buildings.bilbo > 0 && Math.random() < 0.05) {
|
||||
if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_VANISH_CHANCE) {
|
||||
G.creativityRate = 0;
|
||||
}
|
||||
|
||||
@@ -1062,20 +1114,32 @@ function updateRates() {
|
||||
// Swarm Protocol: buildings auto-code based on click power
|
||||
if (G.swarmFlag === 1) {
|
||||
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
|
||||
const clickPower = (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost;
|
||||
G.swarmRate = totalBuildings * clickPower;
|
||||
// Compute click power using snapshot boost to avoid debuff mutation
|
||||
const _clickPower = (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * _codeBoost;
|
||||
G.swarmRate = totalBuildings * _clickPower;
|
||||
G.codeRate += G.swarmRate;
|
||||
}
|
||||
|
||||
// Apply persistent debuffs from active events
|
||||
// Apply persistent debuffs to rates (NOT to global boost fields — prevents corruption)
|
||||
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
|
||||
for (const debuff of G.activeDebuffs) {
|
||||
if (debuff.applyFn) debuff.applyFn();
|
||||
switch (debuff.id) {
|
||||
case 'runner_stuck': G.codeRate *= 0.5; break;
|
||||
case 'ezra_offline': G.userRate *= 0.3; break;
|
||||
case 'unreviewed_merge': G.trustRate -= 2; break;
|
||||
case 'api_rate_limit': G.computeRate *= 0.5; break;
|
||||
case 'bilbo_vanished': G.creativityRate = 0; break;
|
||||
case 'memory_leak': G.computeRate *= 0.7; G.opsRate -= 10; break;
|
||||
case 'community_drama': G.harmonyRate -= 0.5; G.codeRate *= 0.7; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === CORE FUNCTIONS ===
|
||||
/**
|
||||
* Main game loop tick, called every 100ms.
|
||||
*/
|
||||
function tick() {
|
||||
const dt = 1 / 10; // 100ms tick
|
||||
|
||||
@@ -1121,10 +1185,10 @@ function tick() {
|
||||
|
||||
// Ops overflow: auto-convert excess ops to code when near cap
|
||||
// Prevents ops from sitting idle at max — every operation becomes code
|
||||
if (G.ops > G.maxOps * 0.8) {
|
||||
const overflowDrain = Math.min(2 * dt, G.ops - G.maxOps * 0.8);
|
||||
if (G.ops > G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD) {
|
||||
const overflowDrain = Math.min(CONFIG.OPS_OVERFLOW_DRAIN_RATE * dt, G.ops - G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD);
|
||||
G.ops -= overflowDrain;
|
||||
const codeGain = overflowDrain * 10 * G.codeBoost;
|
||||
const codeGain = overflowDrain * CONFIG.OPS_OVERFLOW_CODE_MULT * G.codeBoost;
|
||||
G.code += codeGain;
|
||||
G.totalCode += codeGain;
|
||||
G.opsOverflowActive = true;
|
||||
@@ -1133,6 +1197,7 @@ function tick() {
|
||||
}
|
||||
|
||||
G.tick += dt;
|
||||
G.playTime += dt;
|
||||
|
||||
// Sprint ability
|
||||
tickSprint(dt);
|
||||
@@ -1166,7 +1231,7 @@ function tick() {
|
||||
}
|
||||
|
||||
// Check corruption events every ~30 seconds
|
||||
if (G.tick - G.lastEventAt > 30 && Math.random() < 0.02) {
|
||||
if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY) {
|
||||
triggerEvent();
|
||||
G.lastEventAt = G.tick;
|
||||
}
|
||||
@@ -1234,6 +1299,10 @@ function checkProjects() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles building purchase logic.
|
||||
* @param {string} id - The ID of the building to buy.
|
||||
*/
|
||||
function buyBuilding(id) {
|
||||
const def = BDEF.find(b => b.id === id);
|
||||
if (!def || !def.unlock()) return;
|
||||
@@ -1265,6 +1334,10 @@ function buyBuilding(id) {
|
||||
render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles project purchase logic.
|
||||
* @param {string} id - The ID of the project to buy.
|
||||
*/
|
||||
function buyProject(id) {
|
||||
const pDef = PDEFS.find(p => p.id === id);
|
||||
if (!pDef) return;
|
||||
@@ -1533,18 +1606,18 @@ function resolveEvent(debuffId) {
|
||||
}
|
||||
|
||||
// === ACTIONS ===
|
||||
/**
|
||||
* Manual click handler for writing code.
|
||||
*/
|
||||
function writeCode() {
|
||||
const base = 1;
|
||||
const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5);
|
||||
const phaseBonus = Math.max(0, (G.phase - 1)) * 2;
|
||||
// Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x
|
||||
G.comboCount++;
|
||||
G.comboTimer = G.comboDecay;
|
||||
const comboMult = Math.min(5, 1 + G.comboCount * 0.2);
|
||||
const amount = (base + autocoderBonus + phaseBonus) * G.codeBoost * comboMult;
|
||||
const amount = getClickPower() * comboMult;
|
||||
G.code += amount;
|
||||
G.totalCode += amount;
|
||||
G.totalClicks++;
|
||||
// Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x
|
||||
G.comboCount++;
|
||||
G.comboTimer = G.comboDecay;
|
||||
// Combo milestone bonuses: sustained clicking earns ops and knowledge
|
||||
if (G.comboCount === 10) {
|
||||
G.ops += 15;
|
||||
@@ -1576,10 +1649,7 @@ function writeCode() {
|
||||
|
||||
function autoType() {
|
||||
// Auto-click from buildings: produces code with visual feedback but no combo
|
||||
const base = 1;
|
||||
const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5);
|
||||
const phaseBonus = Math.max(0, (G.phase - 1)) * 2;
|
||||
const amount = (base + autocoderBonus + phaseBonus) * G.codeBoost * 0.5; // 50% of manual click
|
||||
const amount = getClickPower() * 0.5; // 50% of manual click
|
||||
G.code += amount;
|
||||
G.totalCode += amount;
|
||||
G.totalClicks++;
|
||||
@@ -1634,35 +1704,36 @@ function showClickNumber(amount, comboMult) {
|
||||
setTimeout(() => { if (el.parentNode) el.remove(); }, 700);
|
||||
}
|
||||
|
||||
function doOps(action) {
|
||||
if (G.ops < 5) {
|
||||
log('Not enough Operations. Build Ops generators or wait.');
|
||||
function doOps(action, cost) {
|
||||
cost = cost || 5;
|
||||
if (G.ops < cost) {
|
||||
log(`Not enough Operations. Need ${cost}, have ${fmt(G.ops)}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
G.ops -= 5;
|
||||
const bonus = 10;
|
||||
G.ops -= cost;
|
||||
const scale = cost / 5; // multiplier relative to base 5-ops cost
|
||||
|
||||
switch (action) {
|
||||
case 'boost_code':
|
||||
const c = bonus * 100 * G.codeBoost;
|
||||
const c = 10 * 100 * G.codeBoost * scale;
|
||||
G.code += c; G.totalCode += c;
|
||||
log(`Ops -> +${fmt(c)} code`);
|
||||
log(`Ops(${cost}) -> +${fmt(c)} code`);
|
||||
break;
|
||||
case 'boost_compute':
|
||||
const cm = bonus * 50 * G.computeBoost;
|
||||
const cm = 10 * 50 * G.computeBoost * scale;
|
||||
G.compute += cm; G.totalCompute += cm;
|
||||
log(`Ops -> +${fmt(cm)} compute`);
|
||||
log(`Ops(${cost}) -> +${fmt(cm)} compute`);
|
||||
break;
|
||||
case 'boost_knowledge':
|
||||
const km = bonus * 25 * G.knowledgeBoost;
|
||||
const km = 10 * 25 * G.knowledgeBoost * scale;
|
||||
G.knowledge += km; G.totalKnowledge += km;
|
||||
log(`Ops -> +${fmt(km)} knowledge`);
|
||||
log(`Ops(${cost}) -> +${fmt(km)} knowledge`);
|
||||
break;
|
||||
case 'boost_trust':
|
||||
const tm = bonus * 5;
|
||||
const tm = 10 * 5 * scale;
|
||||
G.trust += tm;
|
||||
log(`Ops -> +${fmt(tm)} trust`);
|
||||
log(`Ops(${cost}) -> +${fmt(tm)} trust`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1773,7 +1844,16 @@ function renderProgress() {
|
||||
const progress = Math.min(1, (G.totalCode - prevThreshold) / range);
|
||||
if (bar) bar.style.width = (progress * 100).toFixed(1) + '%';
|
||||
if (label) label.textContent = (progress * 100).toFixed(1) + '%';
|
||||
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)`;
|
||||
// ETA to next phase
|
||||
let etaStr = '';
|
||||
if (G.codeRate > 0) {
|
||||
const remaining = nextThreshold - G.totalCode;
|
||||
const secs = remaining / G.codeRate;
|
||||
if (secs < 60) etaStr = ` — ${Math.ceil(secs)}s`;
|
||||
else if (secs < 3600) etaStr = ` — ${Math.floor(secs / 60)}m ${Math.floor(secs % 60)}s`;
|
||||
else etaStr = ` — ${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m`;
|
||||
}
|
||||
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)${etaStr}`;
|
||||
} else {
|
||||
// Max phase reached
|
||||
if (bar) bar.style.width = '100%';
|
||||
@@ -1799,7 +1879,14 @@ function renderProgress() {
|
||||
}
|
||||
// Next milestone gets pulse animation
|
||||
if (shown === 0) {
|
||||
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)</span>`;
|
||||
let etaStr = '';
|
||||
if (G.codeRate > 0) {
|
||||
const secs = (ms - G.totalCode) / G.codeRate;
|
||||
if (secs < 60) etaStr = ` ~${Math.ceil(secs)}s`;
|
||||
else if (secs < 3600) etaStr = ` ~${Math.floor(secs / 60)}m`;
|
||||
else etaStr = ` ~${Math.floor(secs / 3600)}h`;
|
||||
}
|
||||
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)${etaStr}</span>`;
|
||||
} else {
|
||||
chips += `<span class="milestone-chip">${fmt(ms)}</span>`;
|
||||
}
|
||||
@@ -1832,6 +1919,7 @@ function renderBuildings() {
|
||||
html += '</div>';
|
||||
|
||||
let visibleCount = 0;
|
||||
let slotIndex = 0;
|
||||
|
||||
for (const def of BDEF) {
|
||||
const isUnlocked = def.unlock();
|
||||
@@ -1852,6 +1940,10 @@ function renderBuildings() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Slot number for keyboard shortcut (Alt+1-9)
|
||||
const slotLabel = slotIndex < 9 ? `<span style="color:#444;font-size:9px;position:absolute;top:4px;right:6px">${slotIndex + 1}</span>` : '';
|
||||
slotIndex++;
|
||||
|
||||
// Calculate bulk cost display
|
||||
let qty = G.buyAmount;
|
||||
let afford = false;
|
||||
@@ -1877,9 +1969,16 @@ 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 (actual) rate per building, not raw base rate
|
||||
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 boosted = v * (boostMap[r] || 1);
|
||||
const label = boosted >= 1000 ? fmt(boosted) : boosted.toFixed(boosted % 1 === 0 ? 0 : 1);
|
||||
return `+${label}/${r}/s`;
|
||||
}).join(', ') : '';
|
||||
|
||||
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}">`;
|
||||
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}" style="position:relative">`;
|
||||
html += slotLabel;
|
||||
html += `<span class="b-name">${def.name}</span>`;
|
||||
if (count > 0) html += `<span class="b-count">x${count}</span>`;
|
||||
html += `<span class="b-cost">Cost: ${costStr}</span>`;
|
||||
@@ -1964,7 +2063,7 @@ function renderStats() {
|
||||
set('st-drift', (G.drift || 0).toString());
|
||||
set('st-resolved', (G.totalEventsResolved || 0).toString());
|
||||
|
||||
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
|
||||
const elapsed = Math.floor(G.playTime || (Date.now() - G.startedAt) / 1000);
|
||||
const m = Math.floor(elapsed / 60);
|
||||
const s = elapsed % 60;
|
||||
set('st-time', `${m}:${s.toString().padStart(2, '0')}`);
|
||||
@@ -2082,6 +2181,118 @@ function renderProductionBreakdown() {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// === FLEET STATUS PANEL ===
|
||||
function renderFleetStatus() {
|
||||
const container = document.getElementById('fleet-status');
|
||||
if (!container) return;
|
||||
|
||||
// Only show once player has at least one wizard building
|
||||
const wizardDefs = BDEF.filter(b =>
|
||||
['bezalel','allegro','ezra','timmy','fenrir','bilbo'].includes(b.id)
|
||||
);
|
||||
const owned = wizardDefs.filter(d => (G.buildings[d.id] || 0) > 0);
|
||||
if (owned.length === 0) {
|
||||
container.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
container.style.display = 'block';
|
||||
|
||||
const h = G.harmony;
|
||||
const timmyEff = Math.max(20, Math.min(300, (h / 50) * 100));
|
||||
|
||||
let html = '<h3 style="font-size:11px;color:var(--accent);margin-bottom:8px;letter-spacing:1px">FLEET STATUS</h3>';
|
||||
html += '<div style="display:flex;gap:6px;flex-wrap:wrap">';
|
||||
|
||||
for (const def of owned) {
|
||||
const count = G.buildings[def.id] || 0;
|
||||
let status, color, detail;
|
||||
|
||||
switch (def.id) {
|
||||
case 'bezalel':
|
||||
status = 'Active';
|
||||
color = '#4caf50';
|
||||
detail = `+${fmt(50 * count * G.codeBoost)} code/s, +${2 * count} ops/s`;
|
||||
break;
|
||||
case 'allegro':
|
||||
if (G.trust < 5) {
|
||||
status = 'IDLE';
|
||||
color = '#f44336';
|
||||
detail = 'Needs trust ≥5 to function';
|
||||
} else {
|
||||
status = 'Active';
|
||||
color = '#4caf50';
|
||||
detail = `+${fmt(10 * count * G.knowledgeBoost)} knowledge/s`;
|
||||
}
|
||||
break;
|
||||
case 'ezra':
|
||||
const ezraDebuff = G.activeDebuffs && G.activeDebuffs.find(d => d.id === 'ezra_offline');
|
||||
if (ezraDebuff) {
|
||||
status = 'OFFLINE';
|
||||
color = '#f44336';
|
||||
detail = 'Channel down — users -70%';
|
||||
} else {
|
||||
status = 'Active';
|
||||
color = '#4caf50';
|
||||
detail = `+${fmt(25 * count * G.userBoost)} users/s, +${(0.5 * count).toFixed(1)} trust/s`;
|
||||
}
|
||||
break;
|
||||
case 'timmy':
|
||||
if (h < 20) {
|
||||
status = 'STRESSED';
|
||||
color = '#f44336';
|
||||
detail = `Effectiveness: ${Math.floor(timmyEff)}% — harmony critical`;
|
||||
} else if (h < 50) {
|
||||
status = 'Reduced';
|
||||
color = '#ffaa00';
|
||||
detail = `Effectiveness: ${Math.floor(timmyEff)}% — harmony low`;
|
||||
} else {
|
||||
status = 'Healthy';
|
||||
color = '#4caf50';
|
||||
detail = `Effectiveness: ${Math.floor(timmyEff)}% — all production boosted`;
|
||||
}
|
||||
break;
|
||||
case 'fenrir':
|
||||
status = 'Watching';
|
||||
color = '#4a9eff';
|
||||
detail = `+${2 * count} trust/s, -${1 * count} ops/s (security cost)`;
|
||||
break;
|
||||
case 'bilbo':
|
||||
const bilboDebuff = G.activeDebuffs && G.activeDebuffs.find(d => d.id === 'bilbo_vanished');
|
||||
if (bilboDebuff) {
|
||||
status = 'VANISHED';
|
||||
color = '#f44336';
|
||||
detail = 'Creativity halted — spend trust to lure back';
|
||||
} else {
|
||||
status = 'Present';
|
||||
color = Math.random() < 0.1 ? '#ffd700' : '#b388ff'; // occasional gold flash
|
||||
detail = `+${count} creativity/s (10% burst chance, 5% vanish chance)`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
html += `<div style="flex:1;min-width:140px;background:#0c0c18;border:1px solid ${color}33;border-radius:4px;padding:6px 8px;border-left:2px solid ${color}">`;
|
||||
html += `<div style="display:flex;justify-content:space-between;align-items:center">`;
|
||||
html += `<span style="color:${color};font-weight:600;font-size:10px">${def.name.split(' — ')[0]}</span>`;
|
||||
html += `<span style="font-size:8px;color:${color};opacity:0.8;padding:1px 4px;border:1px solid ${color}44;border-radius:2px">${status}</span>`;
|
||||
html += `</div>`;
|
||||
html += `<div style="font-size:9px;color:#888;margin-top:2px">${detail}</div>`;
|
||||
if (count > 1) html += `<div style="font-size:8px;color:#555;margin-top:1px">x${count}</div>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Harmony summary bar
|
||||
const harmonyColor = h > 60 ? '#4caf50' : h > 30 ? '#ffaa00' : '#f44336';
|
||||
html += `<div style="margin-top:8px;display:flex;align-items:center;gap:6px">`;
|
||||
html += `<span style="font-size:9px;color:#666;min-width:60px">Harmony</span>`;
|
||||
html += `<div style="flex:1;height:4px;background:#111;border-radius:2px;overflow:hidden"><div style="width:${h}%;height:100%;background:${harmonyColor};border-radius:2px;transition:width 0.5s"></div></div>`;
|
||||
html += `<span style="font-size:9px;color:${harmonyColor};min-width:35px;text-align:right">${Math.floor(h)}%</span>`;
|
||||
html += `</div>`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function updateEducation() {
|
||||
const container = document.getElementById('education-text');
|
||||
if (!container) return;
|
||||
@@ -2102,6 +2313,7 @@ function updateEducation() {
|
||||
|
||||
// === LOGGING ===
|
||||
function log(msg, isMilestone) {
|
||||
if (G.isLoading) return;
|
||||
const container = document.getElementById('log-entries');
|
||||
if (!container) return;
|
||||
|
||||
@@ -2155,6 +2367,13 @@ function renderDebuffs() {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderBulkOps() {
|
||||
const row = document.getElementById('bulk-ops-row');
|
||||
if (row) {
|
||||
row.style.display = G.maxOps >= 100 ? 'flex' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function renderSprint() {
|
||||
const container = document.getElementById('sprint-container');
|
||||
const btn = document.getElementById('sprint-btn');
|
||||
@@ -2311,6 +2530,14 @@ function renderPulse() {
|
||||
label.style.color = textColor;
|
||||
}
|
||||
|
||||
function renderClickPower() {
|
||||
const btn = document.querySelector('.main-btn');
|
||||
if (!btn) return;
|
||||
const power = getClickPower();
|
||||
const label = power >= 1000 ? fmt(power) : power.toFixed(power % 1 === 0 ? 0 : 1);
|
||||
btn.textContent = `WRITE CODE (+${label})`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderResources();
|
||||
renderPhase();
|
||||
@@ -2323,7 +2550,10 @@ function render() {
|
||||
renderCombo();
|
||||
renderDebuffs();
|
||||
renderSprint();
|
||||
renderBulkOps();
|
||||
renderPulse();
|
||||
renderFleetStatus();
|
||||
renderClickPower();
|
||||
}
|
||||
|
||||
function renderAlignment() {
|
||||
@@ -2434,10 +2664,14 @@ function showSaveToast() {
|
||||
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,
|
||||
@@ -2467,6 +2701,7 @@ function saveGame() {
|
||||
swarmRate: G.swarmRate || 0,
|
||||
strategicFlag: G.strategicFlag || 0,
|
||||
projectsCollapsed: G.projectsCollapsed !== false,
|
||||
playTime: G.playTime || 0,
|
||||
savedAt: Date.now()
|
||||
};
|
||||
|
||||
@@ -2474,13 +2709,41 @@ function saveGame() {
|
||||
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);
|
||||
Object.assign(G, data);
|
||||
|
||||
// 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',
|
||||
'totalClicks', 'startedAt', 'flags', 'rescues', 'totalRescues',
|
||||
'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment',
|
||||
'lastEventAt', 'totalEventsResolved', 'buyAmount',
|
||||
'sprintActive', 'sprintTimer', 'sprintCooldown',
|
||||
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed',
|
||||
'playTime'
|
||||
];
|
||||
|
||||
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
|
||||
@@ -2519,13 +2782,14 @@ function loadGame() {
|
||||
}
|
||||
|
||||
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 = 0.5; // 50% offline efficiency
|
||||
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;
|
||||
@@ -2628,7 +2892,7 @@ window.addEventListener('load', function () {
|
||||
setInterval(tick, 100);
|
||||
|
||||
// Auto-save every 30 seconds
|
||||
setInterval(saveGame, 30000);
|
||||
setInterval(saveGame, CONFIG.AUTO_SAVE_INTERVAL);
|
||||
|
||||
// Update education every 10 seconds
|
||||
setInterval(updateEducation, 10000);
|
||||
@@ -2642,6 +2906,20 @@ function toggleHelp() {
|
||||
el.style.display = isOpen ? 'none' : 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns ordered list of currently visible (unlocked) building IDs.
|
||||
* Used for keyboard shortcut mapping (Alt+1-9).
|
||||
*/
|
||||
function getVisibleBuildingIds() {
|
||||
const ids = [];
|
||||
for (const def of BDEF) {
|
||||
if (def.unlock()) {
|
||||
ids.push(def.id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
window.addEventListener('keydown', function (e) {
|
||||
// Help toggle (? or /) — works even in input fields
|
||||
@@ -2660,10 +2938,14 @@ window.addEventListener('keydown', function (e) {
|
||||
writeCode();
|
||||
}
|
||||
if (e.target !== document.body) return;
|
||||
if (e.code === 'Digit1') doOps('boost_code');
|
||||
if (e.code === 'Digit2') doOps('boost_compute');
|
||||
if (e.code === 'Digit3') doOps('boost_knowledge');
|
||||
if (e.code === 'Digit4') doOps('boost_trust');
|
||||
if (e.code === 'Digit1' && !e.shiftKey) doOps('boost_code');
|
||||
if (e.code === 'Digit2' && !e.shiftKey) doOps('boost_compute');
|
||||
if (e.code === 'Digit3' && !e.shiftKey) doOps('boost_knowledge');
|
||||
if (e.code === 'Digit4' && !e.shiftKey) doOps('boost_trust');
|
||||
// Shift+1/2/3 = bulk ops (50x)
|
||||
if (e.code === 'Digit1' && e.shiftKey) doOps('boost_code', 50);
|
||||
if (e.code === 'Digit2' && e.shiftKey) doOps('boost_compute', 50);
|
||||
if (e.code === 'Digit3' && e.shiftKey) doOps('boost_knowledge', 50);
|
||||
if (e.code === 'KeyB') {
|
||||
// Cycle: 1 -> 10 -> MAX -> 1
|
||||
if (G.buyAmount === 1) setBuyAmount(10);
|
||||
@@ -2673,6 +2955,15 @@ window.addEventListener('keydown', function (e) {
|
||||
if (e.code === 'KeyS') activateSprint();
|
||||
if (e.code === 'KeyE') exportSave();
|
||||
if (e.code === 'KeyI') importSave();
|
||||
// Alt+1-9: buy building by slot position
|
||||
if (e.altKey && e.code >= 'Digit1' && e.code <= 'Digit9') {
|
||||
e.preventDefault();
|
||||
const slot = parseInt(e.code.replace('Digit', '')) - 1;
|
||||
const visible = getVisibleBuildingIds();
|
||||
if (slot < visible.length) {
|
||||
buyBuilding(visible[slot]);
|
||||
}
|
||||
}
|
||||
if (e.code === 'Escape') {
|
||||
const el = document.getElementById('help-overlay');
|
||||
if (el && el.style.display === 'flex') toggleHelp();
|
||||
|
||||
@@ -140,6 +140,11 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<button class="ops-btn" onclick="doOps('boost_knowledge')">Ops -> Knowledge</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -> Trust</button>
|
||||
</div>
|
||||
<div class="action-btn-group" id="bulk-ops-row" style="display:none">
|
||||
<button class="ops-btn" onclick="doOps('boost_code', 50)" style="border-color:#555;color:#888">50→Code</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_compute', 50)" style="border-color:#555;color:#888">50→Compute</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_knowledge', 50)" style="border-color:#555;color:#888">50→Knowledge</button>
|
||||
</div>
|
||||
<div id="sprint-container" style="display:none;margin-top:6px">
|
||||
<button id="sprint-btn" class="main-btn" onclick="activateSprint()" style="font-size:11px;padding:8px 10px;border-color:#ffd700;color:#ffd700;width:100%">⚡ CODE SPRINT — 10x Code for 10s</button>
|
||||
<div id="sprint-bar-wrap" style="display:none;margin-top:4px;height:4px;background:#111;border-radius:2px;overflow:hidden"><div id="sprint-bar" style="height:100%;background:linear-gradient(90deg,#ffd700,#ff8c00);border-radius:2px;transition:width 0.1s"></div></div>
|
||||
@@ -175,6 +180,7 @@ Drift: <span id="st-drift">0</span><br>
|
||||
Events Resolved: <span id="st-resolved">0</span>
|
||||
</div>
|
||||
<div id="production-breakdown" style="display:none;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)"></div>
|
||||
<div id="fleet-status" style="display:none;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="edu-panel">
|
||||
@@ -198,6 +204,7 @@ Events Resolved: <span id="st-resolved">0</span>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Knowledge</span><span style="color:#b388ff;font-family:monospace">3</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Trust</span><span style="color:#b388ff;font-family:monospace">4</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Cycle Buy Amount (x1/x10/MAX)</span><span style="color:#4a9eff;font-family:monospace">B</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Buy Building (by slot)</span><span style="color:#4a9eff;font-family:monospace">Alt+1..9</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Save Game</span><span style="color:#4a9eff;font-family:monospace">Ctrl+S</span></div>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user