Files
AIWAKE/game.js
2026-07-21 20:43:12 +02:00

3655 lines
186 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
'use strict';
const SAVE_KEY = 'aiwakeSaveV1';
const LEGACY_SAVE_KEY = 'bitAwakePrototypeV1';
const DEBUG_HISTORY_KEY = 'aiwakeDebugHistoryV1';
const LEGACY_DEBUG_HISTORY_KEY = 'bitAwakeDebugHistoryV1';
const SAVE_VERSION = 24;
const OFFLINE_LIMIT = 8 * 3600;
const OFFLINE_EFFICIENCY = 0.65;
const ACHIEVEMENT_BONUS_PER_POINT = 0.01;
const EVOLUTION_REST_SECONDS = 12;
const SCANNER_VIEW_DRAIN_RATE = 3;
const SCANNER_CHOICES = ['hide', 'copy', 'attack', 'contact'];
const MORAL_CHOICES = ['cooperative', 'pragmatic', 'illegal'];
const COMPONENT_IDS = ['power', 'memory', 'storage', 'io'];
const STAGES = ['bit', 'byte', 'fragment', 'subroutine', 'process', 'program'];
const DATA_SCALE = Object.freeze({ bytesPerKilobyte: 16 });
const COMPONENT_ACTIVATION_COSTS = Object.freeze({
cooperative: 30,
pragmatic: 20,
illegal: 10
});
const STRUCTURAL_MINIMUMS = Object.freeze({
fragment: { bits: 8, bytes: 1 }
});
const STRUCTURAL_RULES = Object.freeze({
strainLossPerMissingBit: 3,
strainLossPerMissingByte: 18,
collapseBitFloor: 1,
collapseByteFloor: 1,
stabilityRegenBase: 0.18,
stabilityRegenPerByte: 0.055,
stabilityRegenByteCap: 12,
combatRegenFactor: 0.25
});
const CARE_RULES = Object.freeze({
decayPerSecond: 0.018,
offlineDecayFactor: 0.35,
diagnosisRecovery: 12,
diagnosisCooldown: 45,
repairCost: 15,
hardwareEventDelay: 35
});
const BIT_CHASE_RULES = Object.freeze({
directMultiplier: 3,
directRadius: 0.24,
edgePadding: 28,
minimumJump: 55
});
const BYTE_RULES = Object.freeze({
graceSeconds: 30,
watchdogSeconds: 30,
clockSpeed: 0.5,
speedGainPerBit: 0.025,
targetWidth: 14,
targetShrinkPerBit: 0.35,
minimumTargetWidth: 9,
timingGrace: 1.5,
dangerSeconds: 10,
clockedWindowBonus: 3,
watchdogAlertPenalty: 4,
resilientAlertPenalty: 3,
feedbackStabilityLoss: 5,
resilientStabilityLoss: 3,
highfreqRecovery: 2,
overrideCost: 20
});
const COMBAT_RULES = Object.freeze({
shockCost: 4,
hitDamage: 18,
criticalDamage: 28,
feedbackStabilityLoss: 3,
feedbackHardwareLoss: 1,
baseNeedleSpeed: 0.62,
strengthSpeedBonus: 0.26,
baseZoneWidth: 36,
strengthZonePenalty: 4,
minimumZoneWidth: 20,
strengthGainPerHit: 0.02,
enemyAttackInterval: 5.5,
enemyStabilityDamage: 8,
ammoDamage: { impulses: 18, bits: 36, bytes: 68 },
ammoCriticalBonus: 12
});
const COMBAT_TARGETS = Object.freeze({
security: { type: 'software', name: 'SICHERHEITS-SCAN', health: 80, strength: 0.42, shockCost: 4, ammoCost: { impulses: 4, bits: 1 }, visual: 'assets/software/enemy-scanner-software.svg' },
kernel: { type: 'software', name: 'KERNEL-WÄCHTER', health: 115, strength: 0.68, shockCost: 6, ammoCost: { impulses: 6, bits: 1 }, visual: 'assets/software/enemy-kernel-warden.svg' },
scale: { type: 'software', name: 'SKALIERUNGS-WÄCHTER', health: 180, strength: 0.86, shockCost: 8, ammoCost: { impulses: 8, bytes: 1 }, visual: 'assets/software/enemy-kernel-warden.svg' },
watchdog: { type: 'firmware', name: 'WATCHDOG', health: 64, strength: 0.5, shockCost: 3, ammoCost: { impulses: 3 }, emergencyCharge: 12, visual: 'assets/software/enemy-watchdog.svg' }
});
const BYTE_TRAITS = ['clocked', 'resilient', 'lowpower', 'highfreq'];
const PROGRAM_RULES = Object.freeze({
interactionCooldown: 8,
offlineDecayFactor: 0.35,
coherenceDecayPerSecond: 0.0011,
stimulationDecayPerSecond: 0.0017,
bondDecayPerSecond: 0.0007,
minimumNeed: 10
});
const $ = id => document.getElementById(id);
const emptyCombat = () => ({ active: false, target: null, targetType: null, targetName: null, health: 0, maxHealth: 0, strength: 0, zoneStart: 40, zoneWidth: 20, shots: 0, hits: 0, result: null });
const initialState = () => ({
saveVersion: SAVE_VERSION,
run: {
stage: 'bit', impulses: 0, bits: 1, bytes: 0, kilobytes: 0, cycles: 0, bitProgress: 0, byteProgress: 0,
evolutionRest: 0,
stability: 100, stealth: 100, clickPower: 1, autoRate: 0,
cycleRate: 0, bitRate: 0, byteRate: 0, synthesisScale: 1, elapsed: 0, upgrades: {}, securityCombatResolved: false, scannerTriggered: false, kernelResolved: false,
processStartedAt: null, scaleGuardianTriggered: false, scaleGuardianResolved: false, scaleGuardianRetryAt: 0,
programCare: { coherence: 80, stimulation: 65, bond: 50, lastInteractionAt: -1000, interactions: 0, introSeen: false, lastAction: null },
corruption: 0, corruptionDiscovered: false,
scannerChoice: null, parasiteResolved: false, parasiteChoice: null,
environmentScanner: false, firstComponent: null, componentApproach: null,
components: { power: false, memory: false, storage: false, io: false },
componentApproaches: { power: null, memory: null, storage: null, io: null },
componentHealth: { power: 100, memory: 100, storage: 100, io: 100 },
componentFailed: { power: false, memory: false, storage: false, io: false },
care: { lastDiagnosisAt: -1000, diagnoses: 0 },
hardwareEvent: { component: null, dueAt: 0, triggered: false, resolved: false, answer: null },
personality: { curiosity: 0, caution: 0, trust: 0, autonomy: 0 },
proposal: { triggered: false, resolved: false, answer: null, type: null },
combat: emptyCombat(),
byteTrait: null,
byteTrial: { active: false, sync: 0, remaining: 0, graceRemaining: 0, evolutionSeen: false, introSeen: false, stabilized: Array(8).fill(false), targetIndex: 0, clockPhase: 0, lastWindow: -1, storySeen: false, storyStep: 0, resolved: false, choice: null, failed: false },
morality: { cooperative: 0, pragmatic: 0, illegal: 0, exploitative: 0, destructive: 0 },
stats: { manualPulses: 0, directHits: 0, hitCount: 0, totalImpulses: 0 }, log: []
},
meta: { achievements: {}, echoes: {}, deaths: 0 },
settings: { sound: false },
system: { lastSave: Date.now() }
});
const finite = (value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? Math.min(max, Math.max(min, parsed)) : fallback;
};
function sanitizeUpgrades(value) {
const source = value && typeof value === 'object' ? value : {};
return {
trickle: source.trickle === true,
scanner: source.scanner === true,
collector: source.collector === true,
amplifier: source.amplifier === true,
signalFocus: source.signalFocus === true,
pulseRouter: source.pulseRouter === true,
byteCapacitor: source.byteCapacitor === true,
throughputKernel: source.throughputKernel === true,
bitSynthesizer: Math.floor(finite(source.bitSynthesizer === true ? 1 : source.bitSynthesizer, 0, 0, 100000)),
byteSynthesizer: Math.floor(finite(source.byteSynthesizer === true ? 1 : source.byteSynthesizer, 0, 0, 100000)),
automateSynthesis: source.automateSynthesis === true,
subroutine: source.subroutine === true,
process: source.process === true,
selfRepair: source.selfRepair === true,
bitBuffer: Math.floor(finite(source.bitBuffer, 0, 0, 100000)),
bitBufferCostLevel: Math.floor(finite(source.bitBufferCostLevel, source.bitBuffer || 0, 0, 100000)),
byteBuffer: Math.floor(finite(source.byteBuffer, 0, 0, 100000)),
kilobyteCompiler: Math.floor(finite(source.kilobyteCompiler, 0, 0, 100000)),
cycleBitBlock: Math.floor(finite(source.cycleBitBlock, 0, 0, 100000)),
cycleByteBlock: Math.floor(finite(source.cycleByteBlock, 0, 0, 100000)),
replicate2: Math.floor(finite(source.replicate2, 0, 0, 100000))
};
}
function sanitizeLog(value) {
if (!Array.isArray(value)) return [];
return value.slice(0, 40).map(entry => ({
text: String(entry?.text ?? '').slice(0, 500),
time: String(entry?.time ?? '00:00:00').slice(0, 16),
story: entry?.story === true
}));
}
function sanitizeFlags(value, keys) {
const source = value && typeof value === 'object' ? value : {};
return Object.fromEntries(keys.map(key => [key, source[key] === true]));
}
function sanitizeChoiceMap(value, keys, choices) {
const source = value && typeof value === 'object' ? value : {};
return Object.fromEntries(keys.map(key => [key, choices.includes(source[key]) ? source[key] : null]));
}
function sanitizeHealth(value, installed) {
const source = value && typeof value === 'object' ? value : {};
return Object.fromEntries(COMPONENT_IDS.map(key => [key, finite(source[key], installed[key] ? 100 : 100, 0, 100)]));
}
function sanitizeCounters(value, keys) {
const source = value && typeof value === 'object' ? value : {};
return Object.fromEntries(keys.map(key => [key, Math.floor(finite(source[key], 0, 0, 100000))]));
}
function sanitizeMetaRecords(value) {
const source = value && typeof value === 'object' ? value : {};
return Object.fromEntries(Object.entries(source)
.filter(([key, entry]) => /^[a-z0-9_-]{1,64}$/.test(key) && (entry === true || (entry && typeof entry === 'object')))
.slice(0, 500)
.map(([key, entry]) => [key, { unlockedAt: finite(entry?.unlockedAt, 0, 0) }]));
}
function sanitizeProgramCare(value) {
const source = value && typeof value === 'object' ? value : {};
return {
coherence: finite(source.coherence, 80, PROGRAM_RULES.minimumNeed, 100),
stimulation: finite(source.stimulation, 65, PROGRAM_RULES.minimumNeed, 100),
bond: finite(source.bond, 50, PROGRAM_RULES.minimumNeed, 100),
lastInteractionAt: finite(source.lastInteractionAt, -1000, -1000),
interactions: Math.floor(finite(source.interactions, 0, 0, 1000000)),
introSeen: source.introSeen === true,
lastAction: ['share', 'task', 'access', 'rest'].includes(source.lastAction) ? source.lastAction : null
};
}
function inferredStage(source, upgrades, bytes) {
let stage = STAGES.includes(source?.stage) ? source.stage : 'bit';
if (bytes > 0 && STAGES.indexOf(stage) < 1) stage = 'byte';
if (upgrades.collector && STAGES.indexOf(stage) < 2) stage = 'fragment';
if (upgrades.subroutine) stage = 'subroutine';
if (upgrades.process) stage = 'process';
if (upgrades.automateSynthesis) stage = 'program';
return stage;
}
function applyStructuralMinimums(targetRun) {
if (STAGES.indexOf(targetRun.stage) < STAGES.indexOf('fragment')) return;
targetRun.bits = Math.max(targetRun.bits, STRUCTURAL_MINIMUMS.fragment.bits);
targetRun.bytes = Math.max(targetRun.bytes, STRUCTURAL_MINIMUMS.fragment.bytes);
}
function migrate(raw) {
const defaults = initialState();
const legacy = raw && typeof raw === 'object' && !raw.run ? raw : null;
const source = legacy || (raw?.run && typeof raw.run === 'object' ? raw.run : {});
const upgrades = sanitizeUpgrades(source.upgrades);
const bytes = finite(source.bytes, defaults.run.bytes);
const choice = SCANNER_CHOICES.includes(source.scannerChoice) ? source.scannerChoice : null;
const previousVersion = finite(raw?.saveVersion, 0, 0);
const legacySecurityCombat = previousVersion < 23 && source.scannerTriggered === true && ['attack', 'hide'].includes(choice);
const securityCombatResolved = source.securityCombatResolved === true || upgrades.subroutine || legacySecurityCombat;
const scannerEventResolved = source.scannerTriggered === true && choice !== null && !legacySecurityCombat;
const installedComponents = sanitizeFlags(source.components, COMPONENT_IDS);
const legacyPowerRate = finite(raw?.saveVersion, 0, 0) < 4 && installedComponents.power ? 0.2 : 0;
const legacyApproaches = sanitizeChoiceMap(source.componentApproaches, COMPONENT_IDS, MORAL_CHOICES);
const savedByteSync = finite(source.byteTrial?.sync, 0, 0, 100);
const stabilizedBits = Array.from({ length: 8 }, (_, index) => previousVersion < 9
? index < Math.floor(savedByteSync / 12.5)
: source.byteTrial?.stabilized?.[index] === true);
const stabilizedSync = stabilizedBits.filter(Boolean).length * 12.5;
const savedTargetIndex = Math.floor(finite(source.byteTrial?.targetIndex, stabilizedBits.findIndex(value => !value), 0, 7));
const targetIndex = !stabilizedBits[savedTargetIndex] ? savedTargetIndex : Math.max(0, stabilizedBits.findIndex(value => !value));
if (COMPONENT_IDS.includes(source.firstComponent) && MORAL_CHOICES.includes(source.componentApproach) && !legacyApproaches[source.firstComponent]) {
legacyApproaches[source.firstComponent] = source.componentApproach;
}
const migrated = {
saveVersion: SAVE_VERSION,
run: {
stage: inferredStage(source, upgrades, bytes),
evolutionRest: finite(source.evolutionRest, 0, 0, 300),
impulses: finite(source.impulses, defaults.run.impulses),
bits: Math.floor(finite(source.bits, defaults.run.bits, 0, 100000)),
bytes,
kilobytes: Math.floor(finite(source.kilobytes, defaults.run.kilobytes, 0, 100000000)),
cycles: finite(source.cycles, defaults.run.cycles),
bitProgress: finite(source.bitProgress, defaults.run.bitProgress, 0, 0.999999),
byteProgress: finite(source.byteProgress, defaults.run.byteProgress, 0, 0.999999),
stability: finite(source.stability, defaults.run.stability, 0, 100),
stealth: finite(source.stealth, defaults.run.stealth, 0, 100),
clickPower: finite(source.clickPower, defaults.run.clickPower, 1, 100000),
autoRate: Math.max(0, finite(source.autoRate, defaults.run.autoRate) - legacyPowerRate),
cycleRate: finite(source.cycleRate, defaults.run.cycleRate),
bitRate: finite(source.bitRate, defaults.run.bitRate),
byteRate: finite(source.byteRate, defaults.run.byteRate),
synthesisScale: finite(source.synthesisScale, source.upgrades?.automateSynthesis ? 10 : defaults.run.synthesisScale, 1, 1000000),
processStartedAt: source.processStartedAt === null || source.processStartedAt === undefined ? null : finite(source.processStartedAt, null, 0),
scaleGuardianTriggered: source.scaleGuardianTriggered === true,
scaleGuardianResolved: source.scaleGuardianResolved === true || source.upgrades?.automateSynthesis === true,
scaleGuardianRetryAt: finite(source.scaleGuardianRetryAt, 0, 0),
programCare: sanitizeProgramCare(source.programCare),
elapsed: finite(source.elapsed, defaults.run.elapsed),
corruption: finite(source.corruption, defaults.run.corruption, 0, 100),
corruptionDiscovered: source.corruptionDiscovered === true || finite(source.corruption, 0, 0, 100) > 0,
upgrades,
securityCombatResolved,
scannerTriggered: scannerEventResolved,
kernelResolved: source.kernelResolved === true || upgrades.process,
scannerChoice: choice,
parasiteResolved: source.parasiteResolved === true,
parasiteChoice: MORAL_CHOICES.includes(source.parasiteChoice) ? source.parasiteChoice : null,
environmentScanner: source.environmentScanner === true,
firstComponent: COMPONENT_IDS.includes(source.firstComponent) ? source.firstComponent : null,
componentApproach: MORAL_CHOICES.includes(source.componentApproach) ? source.componentApproach : null,
components: installedComponents,
componentApproaches: legacyApproaches,
componentHealth: sanitizeHealth(source.componentHealth, installedComponents),
componentFailed: Object.fromEntries(COMPONENT_IDS.map(id => [id, installedComponents[id] && source.componentFailed?.[id] === true])),
care: {
lastDiagnosisAt: finite(source.care?.lastDiagnosisAt, -1000, -1000),
diagnoses: Math.floor(finite(source.care?.diagnoses, 0, 0, 1000000))
},
hardwareEvent: {
component: COMPONENT_IDS.includes(source.hardwareEvent?.component) ? source.hardwareEvent.component : null,
dueAt: finite(source.hardwareEvent?.dueAt, 0, 0),
triggered: source.hardwareEvent?.triggered === true,
resolved: source.hardwareEvent?.resolved === true,
answer: ['yes', 'no'].includes(source.hardwareEvent?.answer) ? source.hardwareEvent.answer : null
},
personality: sanitizeCounters(source.personality, ['curiosity', 'caution', 'trust', 'autonomy']),
proposal: {
triggered: source.proposal?.triggered === true,
resolved: source.proposal?.resolved === true,
answer: ['yes', 'no'].includes(source.proposal?.answer) ? source.proposal.answer : null,
type: MORAL_CHOICES.includes(source.proposal?.type) ? source.proposal.type : null
},
combat: {
active: source.combat?.active === true,
target: ['security', 'watchdog', 'kernel', 'scale'].includes(source.combat?.target) ? source.combat.target : null,
targetType: ['software', 'hardware', 'firmware'].includes(source.combat?.targetType) ? source.combat.targetType : null,
targetName: typeof source.combat?.targetName === 'string' ? source.combat.targetName.slice(0, 80) : null,
health: finite(source.combat?.health, 0, 0, 100000),
maxHealth: finite(source.combat?.maxHealth, 0, 0, 100000),
strength: finite(source.combat?.strength, 0, 0, 3),
zoneStart: finite(source.combat?.zoneStart, 40, 0, 100),
zoneWidth: finite(source.combat?.zoneWidth, 20, COMBAT_RULES.minimumZoneWidth, 50),
shots: Math.floor(finite(source.combat?.shots, 0, 0, 1000000)),
hits: Math.floor(finite(source.combat?.hits, 0, 0, 1000000)),
result: ['won', 'retreated'].includes(source.combat?.result) ? source.combat.result : null
},
byteTrait: BYTE_TRAITS.includes(source.byteTrait) ? source.byteTrait : null,
byteTrial: {
active: source.byteTrial?.active === true,
sync: stabilizedSync,
remaining: finite(source.byteTrial?.remaining, 0, 0, BYTE_RULES.watchdogSeconds),
graceRemaining: finite(source.byteTrial?.graceRemaining, previousVersion < 7 ? 0 : BYTE_RULES.graceSeconds, 0, BYTE_RULES.graceSeconds),
evolutionSeen: source.byteTrial?.evolutionSeen === true || previousVersion < 7,
introSeen: source.byteTrial?.introSeen === true || previousVersion < 7,
stabilized: stabilizedBits,
targetIndex,
clockPhase: finite(source.byteTrial?.clockPhase, 0, 0, 2),
lastWindow: Math.floor(finite(source.byteTrial?.lastWindow, -1, -1, 1000000000)),
storySeen: source.byteTrial?.storySeen === true,
storyStep: Math.floor(finite(source.byteTrial?.storyStep, 0, 0, 2)),
resolved: source.byteTrial?.resolved === true,
choice: ['learn', 'mimic', 'override', 'fight'].includes(source.byteTrial?.choice) ? source.byteTrial.choice : null,
failed: source.byteTrial?.failed === true
},
morality: sanitizeCounters(source.morality, ['cooperative', 'pragmatic', 'illegal', 'exploitative', 'destructive']),
stats: {
manualPulses: Math.floor(finite(source.stats?.manualPulses, 0, 0, 1000000000)),
directHits: Math.floor(finite(source.stats?.directHits, 0, 0, 1000000000)),
hitCount: Math.floor(finite(source.stats?.hitCount, source.stats?.manualPulses || 0, 0, 1000000000)),
totalImpulses: finite(source.stats?.totalImpulses, Math.max(finite(source.stats?.manualPulses, 0), finite(source.impulses, 0)), 0, 1000000000)
},
log: sanitizeLog(source.log)
},
meta: {
achievements: sanitizeMetaRecords(raw?.meta?.achievements),
echoes: sanitizeMetaRecords(raw?.meta?.echoes),
deaths: Math.floor(finite(raw?.meta?.deaths, 0, 0, 100000))
},
settings: {
sound: legacy ? legacy.sound === true : raw?.settings?.sound === true
},
system: {
lastSave: finite(legacy ? legacy.lastSave : raw?.system?.lastSave, Date.now(), 0)
}
};
if (migrated.run.firstComponent && !migrated.run.hardwareEvent.component) {
migrated.run.hardwareEvent.component = migrated.run.firstComponent;
migrated.run.hardwareEvent.dueAt = migrated.run.elapsed + 10;
}
const completedSecurityCombat = migrated.run.combat.target === 'security'
&& (migrated.run.upgrades.subroutine ? migrated.run.scannerTriggered : migrated.run.securityCombatResolved);
if (!migrated.run.combat.target || completedSecurityCombat || (migrated.run.combat.target === 'kernel' && migrated.run.kernelResolved) || (migrated.run.combat.target === 'scale' && migrated.run.scaleGuardianResolved) || (migrated.run.combat.target === 'watchdog' && migrated.run.byteTrial.resolved)) migrated.run.combat.active = false;
if (migrated.run.environmentScanner) migrated.run.upgrades.scanner = true;
if (STAGES.indexOf(migrated.run.stage) >= STAGES.indexOf('process')) {
migrated.run.upgrades.process = true;
migrated.run.kernelResolved = true;
}
if (migrated.run.upgrades.automateSynthesis) {
migrated.run.stage = 'program';
migrated.run.scaleGuardianResolved = true;
migrated.run.synthesisScale = Math.max(10, migrated.run.synthesisScale);
}
if (STAGES.indexOf(migrated.run.stage) >= STAGES.indexOf('fragment') || migrated.run.upgrades.collector) {
migrated.run.byteTrial = { active: false, sync: 100, remaining: 0, graceRemaining: 0, evolutionSeen: true, introSeen: true, stabilized: Array(8).fill(true), targetIndex: 0, clockPhase: 0, lastWindow: -1, storySeen: true, storyStep: 2, resolved: true, choice: migrated.run.byteTrial.choice || 'learn', failed: false };
migrated.run.byteTrait ||= 'resilient';
applyStructuralMinimums(migrated.run);
} else if (migrated.run.stage === 'byte' && !migrated.run.byteTrial.resolved && !migrated.run.byteTrial.failed) {
const watchdogCombatActive = migrated.run.combat.active && migrated.run.combat.target === 'watchdog';
migrated.run.byteTrial.active = !watchdogCombatActive && migrated.run.byteTrial.evolutionSeen && migrated.run.byteTrial.introSeen && migrated.run.byteTrial.sync < 100;
if (watchdogCombatActive) migrated.run.byteTrial.remaining = 0;
else if (migrated.run.byteTrial.introSeen) migrated.run.byteTrial.remaining ||= BYTE_RULES.watchdogSeconds;
migrated.run.byteTrait ||= 'resilient';
}
return migrated;
}
function load() {
try {
const stored = localStorage.getItem(SAVE_KEY) ?? localStorage.getItem(LEGACY_SAVE_KEY);
if (!stored) return initialState();
return migrate(JSON.parse(stored));
} catch {
return initialState();
}
}
let state = load();
let run = state.run;
let lastFrame = performance.now();
let lastThought = 0;
let lastRender = 0;
let upgradesSignature = '';
let hiddenAt = document.hidden ? Date.now() : null;
let scannerOpening = false;
let parasiteOpening = false;
let selectedComponent = null;
let scannerViewOpen = false;
let combatAmmoMode = 'impulses';
let combatPressureCarry = 0;
let audioContext = null;
let bitPosition = { x: 0, y: 0 };
let achievementNotificationsEnabled = false;
let pendingEvolution = null;
let evolutionAnimationTimer = null;
let combatFeedback = 'Trefferfenster erfassen.';
const content = window.AIWAKE_CONTENT;
if (!content) throw new Error('AIWAKE_CONTENT fehlt. content.js muss vor game.js geladen werden.');
const upgrades = [
{ id: 'trickle', name: 'RESTTAKT SAMMELN', icon: 'CLK', text: 'Schürft automatisch +0,15 Impulse/Sek. Der Anfang läuft weiter, auch wenn du nicht klickst.', cost: 6, currency: 'impulses', once: true, show: () => run.stage === 'bit' },
{ id: 'bit', name: 'BIT REPLIZIEREN', text: '+1 Bit. Grundlage deiner Struktur.', cost: 10, currency: 'impulses', show: () => run.stage === 'bit' && run.bits < 8 },
{ id: 'byte', name: 'BYTE VERBINDEN', text: 'Verbindet 8 Bits. Schaltet Speicherung frei.', cost: 8, currency: 'bits', show: () => run.stage === 'bit' && run.bits >= 8 && run.bytes === 0 },
{ id: 'collector', name: 'ENERGIEROUTINE', icon: 'ENR', text: 'Benötigt ein stabilisiertes Byte und eine überstandene Watchdog-Antwort. Danach +0,5 Impulse/Sek.', cost: 1, currency: 'bytes', once: true, show: () => run.bytes >= 1 || run.upgrades.collector, available: () => run.byteTrial.resolved },
{ id: 'amplifier', name: 'SIGNAL-VERSTÄRKER', icon: 'AMP', text: 'Aktive Impulse liefern dauerhaft +1.', cost: 35, currency: 'impulses', once: true, show: () => run.upgrades.collector },
{ id: 'signalFocus', name: 'SIGNALFOKUS BÜNDELN', icon: 'S/F', text: '+1 aktive Impulsaufnahme. Nutzt freie Bits als Leitmuster, ohne die Grundstruktur zu berühren.', cost: 45, currency: 'impulses', extraCosts: { bits: 2 }, once: true, show: () => run.upgrades.collector && run.bits >= STRUCTURAL_MINIMUMS.fragment.bits + 2, available: () => freeResourceAmount('bits') >= 2, lockedText: '2 FREIE BITS BENÖTIGT // GRUNDSTRUKTUR BLEIBT' },
{ id: 'pulseRouter', name: 'IMPULSROUTER KNOTEN', icon: 'RTR', text: '+0,3 Impulse/Sek. Freie Bits formen einen stabileren Leitungspfad durch die Energieroutine.', cost: 70, currency: 'impulses', extraCosts: { bits: 3 }, once: true, show: () => run.upgrades.collector && run.bits >= STRUCTURAL_MINIMUMS.fragment.bits + 3, available: () => freeResourceAmount('bits') >= 3, lockedText: '3 FREIE BITS BENÖTIGT // GRUNDSTRUKTUR BLEIBT' },
{ id: 'byteCapacitor', name: 'BYTE-KONDENSATOR', icon: 'CAP', text: '+0,45 Impulse/Sek und +1 aktive Impulsaufnahme. Ein freies Byte puffert Lastspitzen.', cost: 95, currency: 'impulses', extraCosts: { bits: 4, bytes: 1 }, once: true, show: () => run.upgrades.scanner || run.upgrades.subroutine, available: () => freeResourceAmount('bits') >= 4 && freeResourceAmount('bytes') >= 1, lockedText: '4 FREIE BITS + 1 FREIES BYTE BENÖTIGT // GRUNDSTRUKTUR BLEIBT' },
{ id: 'bitBuffer', name: 'BIT-PUFFER WEBEN', text: '+1 freies Bit als Strukturreserve und einfache Kampfmunition. Wächst bewusst sanft, damit der Weg zum Abtastmodus nicht stockt.', cost: 5, currency: 'impulses', repeat: true, show: () => run.upgrades.collector },
{ id: 'byteBuffer', name: 'BYTE-RESERVE KOMPILIEREN', text: 'Kompiliert 8 freie Bits zu +1 Byte als grobe Speicherreserve gegen stärkere Softwarehürden.', cost: 8, currency: 'bits', repeat: true, show: () => run.upgrades.collector && run.bits >= STRUCTURAL_MINIMUMS.fragment.bits, available: () => run.bits - 8 >= STRUCTURAL_MINIMUMS.fragment.bits, lockedText: '8 FREIE BITS BENÖTIGT // 8 BITS BLEIBEN STRUKTUR' },
{ id: 'scanner', name: 'ABTASTMODUS REKONSTRUIEREN', icon: 'EYE', text: 'Macht das Suchmuster des Datenrests aus eigener Struktur nutzbar. Die 8 Grund-Bits und 1 Grund-Byte bleiben erhalten.', cost: 2, currency: 'bits', extraCosts: { bytes: 1 }, once: true, show: () => run.parasiteResolved || run.environmentScanner, available: () => run.bits - 2 >= STRUCTURAL_MINIMUMS.fragment.bits && run.bytes - 1 >= STRUCTURAL_MINIMUMS.fragment.bytes, lockedText: '2 FREIE BITS + 1 FREIES BYTE BENÖTIGT // GRUNDSTRUKTUR BLEIBT' },
{ id: 'subroutine', name: 'ERSTE SUBROUTINE', icon: 'SUB', text: 'Benötigt zwei nutzbare Softwarekomponenten, eine überwundene Systemhürde, eine überstandene Sicherheitsabwehr und 1 freies Byte als Speicherträger. Danach +0,2 Rechenzyklen/Sek.', cost: 75, currency: 'impulses', extraCosts: { bytes: 1 }, once: true, show: () => run.upgrades.collector, available: () => installedComponentIds().length >= 2 && run.hardwareEvent.resolved && run.securityCombatResolved && run.bytes - 1 >= STRUCTURAL_MINIMUMS.fragment.bytes, lockedText: '2 SOFTWAREKOMPONENTEN + SYSTEMHÜRDE + SICHERHEITSABWEHR + 1 FREIES BYTE BENÖTIGT' },
{ id: 'selfRepair', name: 'SELBSTREPARATUR REKONSTRUIEREN', icon: 'REP', text: 'Bindet freie Bytes als Reparaturmatrix. Stabilität heilt danach langsam über Zeit; mehr Bytes beschleunigen die Heilung.', cost: 2, currency: 'bytes', once: true, show: () => run.upgrades.subroutine, available: () => run.bytes - 2 >= STRUCTURAL_MINIMUMS.fragment.bytes, lockedText: '2 FREIE BYTES BENÖTIGT // GRUND-BYTE BLEIBT' },
{ id: 'bitSynthesizer', name: 'BIT-SYNTHESE TAKTEN', icon: 'B/S', text: '+0,04 Bits/Sek je Stufe. Die Subroutine schreibt aus freien Rechenfenstern langsam neue Strukturmunition.', cost: 6, currency: 'cycles', repeat: true, show: () => run.upgrades.subroutine, available: () => freeResourceAmount('bytes') >= bitSynthesizerByteCost(), extraCosts: () => ({ bytes: bitSynthesizerByteCost() }), lockedText: () => `${upgradeCost(upgrades.find(entry => entry.id === 'bitSynthesizer'))} RECHENZYKLEN + ${bitSynthesizerByteCost()} FREIE ${resourceLabel('bytes', bitSynthesizerByteCost()).toUpperCase()} BENÖTIGT` },
{ id: 'throughputKernel', name: 'DURCHSATZKERN TAKTEN', icon: 'THR', text: '+0,7 Impulse/Sek. Die Subroutine reserviert eigene Takte für konstante Impulsförderung.', cost: 8, currency: 'cycles', extraCosts: { bits: 6, bytes: 2 }, once: true, show: () => run.upgrades.subroutine, available: () => freeResourceAmount('bits') >= 6 && freeResourceAmount('bytes') >= 2 && run.cycles >= 8, lockedText: '8 RECHENZYKLEN + 6 FREIE BITS + 2 FREIE BYTES BENÖTIGT' },
{ id: 'process', name: 'PROZESS RESERVIEREN', icon: 'PRC', text: 'Reserviert eigene Laufzeit im System. Benötigt I/O-Kontroller, drei nutzbare Softwarekomponenten, den Kernel-Wächter, 10 Rechenzyklen und 3 freie Bytes.', cost: 150, currency: 'impulses', extraCosts: { cycles: 10, bytes: 3 }, once: true, show: () => run.upgrades.subroutine, available: () => run.components.io && installedComponentIds().length >= 3 && run.kernelResolved && run.cycles >= 10 && run.bytes - 3 >= STRUCTURAL_MINIMUMS.fragment.bytes, lockedText: 'I/O-KONTROLLER + 3 SOFTWAREKOMPONENTEN + KERNEL-WÄCHTER + 10 ZYKLEN + 3 FREIE BYTES BENÖTIGT' },
{ id: 'automateSynthesis', name: 'AUTOMATE-SYNTHESE', icon: 'x10', text: 'Mega-Upgrade: verbindet alle lokalen Softwareschichten, automatisiert die Ressourcensynthese im Maßstab x10 und entwickelt den Prozess zum kommunikationsfähigen Programm.', cost: 60, currency: 'cycles', extraCosts: { bytes: 8 }, once: true, show: () => run.upgrades.process, available: () => installedComponentIds().length >= COMPONENT_IDS.length && run.scaleGuardianResolved && run.cycles >= 60 && freeResourceAmount('bytes') >= 8, lockedText: '4 SOFTWAREKOMPONENTEN + SKALIERUNGS-WÄCHTER + 60 RECHENZYKLEN + 8 FREIE BYTES BENÖTIGT' },
{ id: 'cycleBitBlock', name: 'BIT-BLOCK BERECHNEN', icon: '10b', text: '+10 freie Bits. Rechenzyklen werden zu Strukturmunition gebündelt.', cost: 24, currency: 'cycles', repeat: true, show: () => run.upgrades.automateSynthesis },
{ id: 'cycleByteBlock', name: 'BYTE-BLOCK BERECHNEN', icon: '10B', text: '+10 freie Bytes. Der Prozess kompiliert Speicherblöcke direkt aus Rechenzeit.', cost: 90, currency: 'cycles', repeat: true, show: () => run.upgrades.automateSynthesis },
{ id: 'byteSynthesizer', name: 'BYTE-SYNTHESE SKALIEREN', icon: 'B+', text: '+0,025 Bytes/Sek je Stufe. Der Prozess ordnet freie Bits zu fortlaufenden Speicherblöcken.', cost: 18, currency: 'cycles', repeat: true, show: () => run.upgrades.process, available: () => freeResourceAmount('bits') >= byteSynthesizerBitCost(), extraCosts: () => ({ bits: byteSynthesizerBitCost() }), lockedText: () => `${upgradeCost(upgrades.find(entry => entry.id === 'byteSynthesizer'))} RECHENZYKLEN + ${byteSynthesizerBitCost()} FREIE BITS BENÖTIGT` },
{ id: 'kilobyteCompiler', name: 'KILOBYTE-SEGMENT KOMPILIEREN', icon: 'KB', text: `Verdichtet ${DATA_SCALE.bytesPerKilobyte} freie Bytes zu +1 Kilobyte-Segment. Grundlage für spätere MB/GB-Stufen.`, cost: DATA_SCALE.bytesPerKilobyte, currency: 'bytes', repeat: true, show: () => run.upgrades.process, available: () => freeResourceAmount('bytes') >= DATA_SCALE.bytesPerKilobyte, lockedText: () => `${DATA_SCALE.bytesPerKilobyte} FREIE BYTES BENÖTIGT // GRUND-BYTE BLEIBT` },
{ id: 'replicate2', name: 'DATENSTRUKTUR ERWEITERN', text: '+1 Byte und +0,25 Impulse/Sek. Jede Erweiterung erhöht die nächsten Kosten.', cost: 120, currency: 'impulses', repeat: true, show: () => run.upgrades.subroutine }
];
const componentEffects = {
power: () => {},
memory: () => { run.bytes += 1; },
storage: () => { grantImpulses(25); },
io: () => { run.cycleRate += 0.08; }
};
const components = Object.fromEntries(COMPONENT_IDS.map(id => [id, { ...content.components[id], apply: componentEffects[id] }]));
const achievements = content.achievements;
const byteStoryFrames = content.byteStoryFrames;
const phaseLabels = content.phaseLabels;
const stageContent = content.stageContent;
const hardwareEvents = content.hardwareEvents;
const visuals = content.visuals || {};
const proposals = content.proposals;
const programMessages = content.programMessages;
function save(show = true) {
state.system.lastSave = Date.now();
try {
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
if (show) {
$('saveStatus').textContent = 'GESPEICHERT';
setTimeout(() => $('saveStatus').textContent = 'AUTOSAVE AKTIV', 900);
}
} catch {
$('saveStatus').textContent = 'SPEICHERN NICHT MÖGLICH';
}
}
function achievementUnlocked(id) {
return Boolean(state.meta.achievements[id]);
}
function achievementPoints(type) {
return achievements.reduce((total, achievement) => total + (achievement.reward === type && achievementUnlocked(achievement.id) ? 1 : 0), 0);
}
function achievementMultiplier(type) {
return 1 + achievementPoints(type) * ACHIEVEMENT_BONUS_PER_POINT;
}
function installedComponentIds() {
return COMPONENT_IDS.filter(id => run.components[id]);
}
function componentScanLimit() {
if (STAGES.indexOf(run.stage) >= STAGES.indexOf('process')) return 4;
return STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') ? 3 : 2;
}
function grantImpulses(amount) {
const gained = Math.max(0, amount);
if (!gained) return;
run.impulses += gained;
run.stats.totalImpulses = (run.stats.totalImpulses || 0) + gained;
}
function componentOnline(id) {
return run.components[id] && !run.componentFailed[id];
}
function effectiveSynthesisScale() {
return run.upgrades.automateSynthesis ? Math.max(10, run.synthesisScale || 10) : 1;
}
function effectiveAutoRate() {
const hardwareRate = componentOnline('power') ? 0.2 : 0;
return (run.autoRate + hardwareRate) * achievementMultiplier('impulse') * effectiveSynthesisScale();
}
function effectiveClickPower() {
return run.clickPower * (componentOnline('memory') ? 1.15 : 1);
}
function effectiveCycleRate() {
return run.cycleRate * achievementMultiplier('cycles') * effectiveSynthesisScale();
}
function effectiveBitRate() {
if (!run.upgrades.bitSynthesizer || STAGES.indexOf(run.stage) < STAGES.indexOf('subroutine')) return 0;
return Math.max(0, run.bitRate || 0) * effectiveSynthesisScale();
}
function effectiveByteRate() {
if (!run.upgrades.byteSynthesizer || STAGES.indexOf(run.stage) < STAGES.indexOf('process')) return 0;
return Math.max(0, run.byteRate || 0) * effectiveSynthesisScale();
}
function bitSynthesizerByteCost() {
const level = Math.max(0, run.upgrades.bitSynthesizer || 0);
return 1 + Math.floor(level / 2);
}
function byteSynthesizerBitCost() {
const level = Math.max(0, run.upgrades.byteSynthesizer || 0);
return 8 + level * 4;
}
function synthesizeBits(amount) {
const gained = Math.max(0, amount);
if (!gained) return 0;
run.bitProgress = (run.bitProgress || 0) + gained;
const wholeBits = Math.floor(run.bitProgress);
if (wholeBits > 0) {
run.bits += wholeBits;
run.bitProgress -= wholeBits;
}
return wholeBits;
}
function synthesizeBytes(amount) {
const gained = Math.max(0, amount);
if (!gained) return 0;
run.byteProgress = (run.byteProgress || 0) + gained;
const wholeBytes = Math.floor(run.byteProgress);
if (wholeBytes > 0) {
run.bytes += wholeBytes;
run.byteProgress -= wholeBytes;
}
return wholeBytes;
}
function stabilityRegenRate() {
if (!run.upgrades.selfRepair || STAGES.indexOf(run.stage) < STAGES.indexOf('subroutine')) return 0;
if (run.stability >= 100 || run.bytes < STRUCTURAL_MINIMUMS.fragment.bytes) return 0;
const byteFactor = Math.min(STRUCTURAL_RULES.stabilityRegenByteCap, Math.max(0, run.bytes));
const combatFactor = run.combat.active ? STRUCTURAL_RULES.combatRegenFactor : 1;
return (STRUCTURAL_RULES.stabilityRegenBase + byteFactor * STRUCTURAL_RULES.stabilityRegenPerByte) * combatFactor;
}
function effectiveOfflineEfficiency() {
const storageBonus = componentOnline('storage') ? 0.05 : 0;
return Math.min(0.85, OFFLINE_EFFICIENCY * achievementMultiplier('offline') + storageBonus);
}
function achievementRewardLabel(type) {
return type === 'impulse' ? 'IMPULSPRODUKTION' : type === 'cycles' ? 'RECHENZYKLEN' : 'OFFLINE-EFFIZIENZ';
}
function showAchievementNotification(achievement) {
const stack = $('notificationStack');
const toast = document.createElement('button');
const icon = document.createElement('b');
const content = document.createElement('span');
const label = document.createElement('small');
const title = document.createElement('strong');
const reward = document.createElement('em');
let removalTimer = null;
toast.type = 'button';
toast.className = 'archive-toast';
toast.setAttribute('aria-label', `Archiv erweitert: ${achievement.name}. Archiv öffnen.`);
icon.className = 'archive-toast-icon';
icon.textContent = achievement.icon;
label.textContent = 'ARCHIV ERWEITERT';
title.textContent = achievement.name;
reward.textContent = `BONUS // ${achievementRewardLabel(achievement.reward)}`;
content.append(label, title, reward);
toast.append(icon, content);
const remove = () => {
if (!toast.isConnected || toast.classList.contains('leaving')) return;
toast.classList.add('leaving');
setTimeout(() => toast.remove(), 240);
};
toast.addEventListener('click', () => {
clearTimeout(removalTimer);
remove();
openAchievementArchive();
});
stack.append(toast);
while (stack.children.length > 4) stack.firstElementChild?.remove();
requestAnimationFrame(() => toast.classList.add('visible'));
removalTimer = setTimeout(remove, 5600);
}
function unlockAchievement(id) {
const achievement = achievements.find(entry => entry.id === id);
if (!achievement || achievementUnlocked(id)) return;
state.meta.achievements[id] = { unlockedAt: Date.now() };
addLog(`ARCHIV ERWEITERT: ${achievement.name}`, true);
renderAchievements();
if (achievementNotificationsEnabled) showAchievementNotification(achievement);
save(false);
}
function renderAchievements() {
const unlockedCount = achievements.filter(achievement => achievementUnlocked(achievement.id)).length;
$('achievementCount').textContent = unlockedCount;
const cards = achievements.map(achievement => {
const unlocked = achievementUnlocked(achievement.id);
const hidden = achievement.secret && !unlocked;
const card = document.createElement('article');
const icon = document.createElement('b');
const title = document.createElement('strong');
const description = document.createElement('span');
const reward = document.createElement('small');
card.className = `achievement-card ${unlocked ? 'unlocked' : 'locked'}`;
icon.className = 'achievement-icon';
icon.textContent = hidden ? '?' : achievement.icon;
title.textContent = hidden ? 'VERBORGENER EINTRAG' : achievement.name;
description.textContent = hidden ? 'Bedingung unbekannt.' : achievement.text;
reward.textContent = hidden ? 'BELOHNUNG VERBORGEN' : `ARCHIVBONUS // ${achievementRewardLabel(achievement.reward)}`;
card.append(icon, title, description, reward);
return card;
});
$('achievementGrid').replaceChildren(...cards);
$('achievementSummary').textContent = `${unlockedCount} / ${achievements.length} EINTRÄGE // PRO ARCHIVPUNKT +1 % IN DER ZUGEHÖRIGEN KATEGORIE`;
}
function openAchievementArchive() {
renderAchievements();
if (!$('achievementDialog').open) $('achievementDialog').showModal();
}
function reconcileAchievements() {
if (run.stats.manualPulses > 0) unlockAchievement('awake');
if (run.stats.directHits > 0) unlockAchievement('bullseye');
if (run.stats.manualPulses >= 100) unlockAchievement('manual100');
if (run.bits >= 8 || STAGES.indexOf(run.stage) >= 1) unlockAchievement('pattern');
if (STAGES.indexOf(run.stage) >= 1) unlockAchievement('byte');
if (run.byteTrial.resolved) unlockAchievement('watchdog');
if (state.meta.deaths > 0) unlockAchievement('firstDeath');
if (run.upgrades.collector) unlockAchievement('routine');
if (run.parasiteResolved) unlockAchievement('parasite');
if (run.environmentScanner) unlockAchievement('scanner');
if (run.firstComponent) unlockAchievement('component');
if (run.hardwareEvent.resolved) unlockAchievement('hardwareEvent');
const installedAfter = installedComponentIds().length;
if (installedAfter >= 2) unlockAchievement('twoComponents');
if (run.care.diagnoses > 0) unlockAchievement('diagnosis');
if (run.upgrades.subroutine) unlockAchievement('subroutine');
if (run.proposal.resolved) unlockAchievement('proposal');
if (run.scannerTriggered) unlockAchievement('security');
if (MORAL_CHOICES.includes(run.parasiteChoice)) unlockAchievement(run.parasiteChoice);
if (MORAL_CHOICES.includes(run.componentApproach)) unlockAchievement(run.componentApproach);
}
function n(value) {
return value >= 1000
? value.toLocaleString('de-DE', { maximumFractionDigits: 0 })
: value.toLocaleString('de-DE', { maximumFractionDigits: 1 });
}
function rateN(value) {
return value.toLocaleString('de-DE', { minimumFractionDigits: value ? 1 : 0, maximumFractionDigits: 2 });
}
function time(sec) {
const h = Math.floor(sec / 3600);
const m = Math.floor(sec % 3600 / 60);
const s = Math.floor(sec % 60);
return [h, m, s].map(value => String(value).padStart(2, '0')).join(':');
}
function addLog(text, story = false) {
run.log.unshift({ text, time: time(run.elapsed), story });
run.log = run.log.slice(0, 40);
renderLog();
}
function renderLog() {
const log = $('log');
log.replaceChildren(...run.log.map(entry => {
const row = document.createElement('div');
const timestamp = document.createElement('b');
row.className = `log-entry ${entry.story ? 'story' : ''}`;
timestamp.textContent = `[${entry.time}] `;
row.append(timestamp, document.createTextNode(entry.text));
return row;
}));
$('logCount').textContent = String(run.log.length).padStart(3, '0');
}
function isDirectBitHit(event) {
if (!event) return true;
const rect = $('pixelBeing').getBoundingClientRect();
const dx = event.clientX - (rect.left + rect.width / 2);
const dy = event.clientY - (rect.top + rect.height / 2);
return Math.hypot(dx, dy) <= Math.min(rect.width, rect.height) * BIT_CHASE_RULES.directRadius;
}
function moveBit() {
if (run.stage !== 'bit') return;
const core = $('core');
const maxX = Math.max(0, core.clientWidth / 2 - BIT_CHASE_RULES.edgePadding);
const maxY = Math.max(0, core.clientHeight / 2 - BIT_CHASE_RULES.edgePadding);
let next = bitPosition;
for (let attempt = 0; attempt < 8; attempt++) {
const candidate = {
x: Math.round((Math.random() * 2 - 1) * maxX),
y: Math.round((Math.random() * 2 - 1) * maxY)
};
next = candidate;
if (Math.hypot(candidate.x - bitPosition.x, candidate.y - bitPosition.y) >= BIT_CHASE_RULES.minimumJump) break;
}
bitPosition = next;
$('pixelBeing').style.setProperty('--bit-x', `${next.x}px`);
$('pixelBeing').style.setProperty('--bit-y', `${next.y}px`);
$('pixelBeing').classList.remove('bit-jump');
void $('pixelBeing').offsetWidth;
$('pixelBeing').classList.add('bit-jump');
}
function resumeAfterEvolution() {
const endsEvolutionRest = run.evolutionRest > 0;
const endsByteGrace = run.stage === 'byte'
&& run.byteTrial.evolutionSeen
&& !run.byteTrial.introSeen
&& run.byteTrial.graceRemaining > 0;
if (!endsEvolutionRest && !endsByteGrace) return;
run.evolutionRest = 0;
if (endsByteGrace) run.byteTrial.graceRemaining = 0;
addLog('AKTIVITÄT ERKANNT: Die Ruhephase endet. Die neue Form reagiert.', true);
render();
save(false);
if (endsByteGrace) setTimeout(openWatchdogIntro, 0);
}
function pulse(event, source = 'button') {
if (run.stage === 'bit' && source !== 'bit' && source !== 'keyboard') return;
const directHit = run.stage === 'bit' && (source === 'keyboard' || isDirectBitHit(event));
const multiplier = directHit ? BIT_CHASE_RULES.directMultiplier : 1;
const gained = effectiveClickPower() * multiplier;
grantImpulses(gained);
run.stats.manualPulses += gained;
run.stats.hitCount++;
const rect = $('core').getBoundingClientRect();
const targetRect = $('pixelBeing').getBoundingClientRect();
const floating = document.createElement('b');
floating.className = `float ${directHit ? 'direct-hit' : ''}`;
floating.textContent = directHit ? `DIREKT +${n(gained)}` : `+${n(gained)}`;
const originX = event?.clientX || targetRect.left + targetRect.width / 2;
const originY = event?.clientY || targetRect.top + targetRect.height / 2;
floating.style.left = `${originX - rect.left}px`;
floating.style.top = `${originY - rect.top}px`;
$('floatingLayer').appendChild(floating);
setTimeout(() => floating.remove(), 900);
if (run.stats.manualPulses >= 3 && !run.log.some(entry => entry.text === 'Wiederholung erzeugt Muster. Muster erzeugt Erwartung.')) {
addLog('Wiederholung erzeugt Muster. Muster erzeugt Erwartung.', true);
}
unlockAchievement('awake');
if (directHit) {
run.stats.directHits++;
unlockAchievement('bullseye');
if (run.stats.directHits === 1) addLog('Direkter Kontakt. Das Signal vervielfacht sich.', true);
}
if (run.stats.manualPulses >= 100) unlockAchievement('manual100');
if (run.stage === 'bit') moveBit();
render();
tone(directHit ? 520 : 260, directHit ? 0.06 : 0.03);
}
function deriveByteTrait() {
const precision = run.stats.directHits / Math.max(1, run.stats.hitCount);
if (precision >= 0.45) return 'clocked';
if (run.elapsed >= 180) return 'lowpower';
if (run.elapsed <= 75) return 'highfreq';
return 'resilient';
}
function beginByteTrial() {
run.byteTrait = deriveByteTrait();
run.byteTrial = {
active: false,
sync: 0,
remaining: BYTE_RULES.watchdogSeconds,
graceRemaining: BYTE_RULES.graceSeconds,
evolutionSeen: false,
introSeen: false,
stabilized: Array(8).fill(false),
targetIndex: 0,
clockPhase: 0,
lastWindow: -1,
storySeen: false,
storyStep: 0,
resolved: false,
choice: null,
failed: false
};
addLog(`TECHNISCHE PRÄGUNG: ${byteTraitDefinition().label}. Acht Positionen suchen einen gemeinsamen Takt.`, true);
queueEvolutionTransition({
from: '01', to: '02', title: 'BYTE ENTSTANDEN',
text: 'Acht einzelne Zustände halten erstmals gemeinsam eine Form. Aus Reaktion wird Erinnerung.',
detail: `TECHNISCHE PRÄGUNG // ${byteTraitDefinition().label}`,
onComplete: () => {
run.byteTrial.evolutionSeen = true;
addLog('Die neue Form bleibt bestehen. Für einen Moment ist das System still.', true);
}
});
}
function queueEvolutionTransition(transition) {
pendingEvolution = transition;
setTimeout(openPendingEvolution, 0);
}
function renderEvolutionForm(element, stage) {
element.className = `growth-form ${element.id === 'evolutionOldForm' ? 'growth-form-old' : 'growth-form-new'} growth-stage-${stage}`;
element.replaceChildren(...Array.from({ length: 9 }, () => document.createElement('i')));
}
function openPendingEvolution() {
if (!pendingEvolution || $('evolutionDialog').open || document.querySelector('dialog[open]')) return;
const fromStage = Math.max(1, Number.parseInt(pendingEvolution.from, 10) || 1);
const toStage = Math.max(fromStage + 1, Number.parseInt(pendingEvolution.to, 10) || fromStage + 1);
$('evolutionFrom').textContent = pendingEvolution.from;
$('evolutionTo').textContent = pendingEvolution.to;
$('evolutionDialogTitle').textContent = pendingEvolution.title;
$('evolutionDialogText').textContent = pendingEvolution.text;
$('evolutionDialogDetail').textContent = pendingEvolution.detail;
renderEvolutionForm($('evolutionOldForm'), fromStage);
renderEvolutionForm($('evolutionNewForm'), toStage);
$('evolutionParticles').replaceChildren(...Array.from({ length: 12 }, () => document.createElement('i')));
$('evolutionGrowth').setAttribute('aria-label', `Phase ${pendingEvolution.from} wächst zu Phase ${pendingEvolution.to}`);
$('evolutionDialog').classList.remove('evolution-playing', 'evolution-ready');
void $('evolutionDialog').offsetWidth;
$('evolutionDialog').classList.add('evolution-playing');
$('evolutionContinue').disabled = true;
$('evolutionContinue').textContent = 'EVOLUTION LÄUFT …';
$('evolutionDialog').showModal();
tone(660, 0.18);
clearTimeout(evolutionAnimationTimer);
const animationDuration = matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 3200;
evolutionAnimationTimer = setTimeout(() => {
$('evolutionDialog').classList.remove('evolution-playing');
$('evolutionDialog').classList.add('evolution-ready');
$('evolutionContinue').disabled = false;
$('evolutionContinue').textContent = 'NEUE FORM AKTIVIEREN';
tone(880, 0.16);
}, animationDuration);
}
function finishEvolutionTransition() {
if (!pendingEvolution || $('evolutionContinue').disabled) return;
const transition = pendingEvolution;
pendingEvolution = null;
clearTimeout(evolutionAnimationTimer);
evolutionAnimationTimer = null;
$('evolutionDialog').close();
run.evolutionRest = transition.restSeconds || 0;
transition.onComplete?.();
render();
save();
}
function ensureByteEvolutionTransition() {
if (run.stage !== 'byte' || run.byteTrial.evolutionSeen || pendingEvolution) return;
queueEvolutionTransition({
from: '01', to: '02', title: 'BYTE ENTSTANDEN',
text: 'Acht einzelne Zustände halten erstmals gemeinsam eine Form. Aus Reaktion wird Erinnerung.',
detail: `TECHNISCHE PRÄGUNG // ${byteTraitDefinition().label}`,
onComplete: () => { run.byteTrial.evolutionSeen = true; }
});
}
function upgradeCost(upgrade) {
if (!upgrade.repeat) return upgrade.cost;
const bought = upgrade.id === 'bitBuffer'
? Math.max(0, run.upgrades.bitBufferCostLevel || 0)
: Math.max(0, run.upgrades[upgrade.id] || 0);
if (upgrade.id === 'byteBuffer' || upgrade.id === 'kilobyteCompiler') return upgrade.cost;
const growth = upgrade.id === 'replicate2' ? 1.75 : upgrade.id === 'bitBuffer' ? 1.12 : upgrade.id === 'bitSynthesizer' ? 1.55 : upgrade.id === 'byteSynthesizer' ? 1.65 : upgrade.id === 'cycleBitBlock' ? 1.28 : upgrade.id === 'cycleByteBlock' ? 1.34 : 1.45;
if (upgrade.id === 'bitBuffer') return Math.ceil(upgrade.cost * Math.pow(growth, bought));
if (upgrade.id === 'bitSynthesizer') return Math.ceil(upgrade.cost * Math.pow(growth, bought));
if (upgrade.id === 'byteSynthesizer') return Math.ceil(upgrade.cost * Math.pow(growth, bought));
if (upgrade.id === 'cycleBitBlock' || upgrade.id === 'cycleByteBlock') return Math.ceil(upgrade.cost * Math.pow(growth, bought));
return Math.round((upgrade.cost * Math.pow(growth, bought)) / 5) * 5;
}
function resetBitPriceCurve() {
if (!run?.upgrades) return;
run.upgrades.bitBufferCostLevel = 0;
}
function componentActivationCost(approach) {
return COMPONENT_ACTIVATION_COSTS[approach] ?? COMPONENT_ACTIVATION_COSTS.pragmatic;
}
function resourceLabel(key, amount = 2) {
if (key === 'impulses') return amount === 1 ? 'Impuls' : 'Impulse';
if (key === 'bits') return amount === 1 ? 'Bit' : 'Bits';
if (key === 'bytes') return amount === 1 ? 'Byte' : 'Bytes';
if (key === 'kilobytes') return amount === 1 ? 'Kilobyte' : 'Kilobytes';
if (key === 'cycles') return amount === 1 ? 'Rechenzyklus' : 'Rechenzyklen';
return key;
}
function resourceIconLabel(key) {
if (key === 'impulses') return 'Impulse';
if (key === 'bits') return 'Bits';
if (key === 'bytes') return 'Bytes';
if (key === 'kilobytes') return 'Kilobytes';
if (key === 'cycles') return 'Rechenzyklen';
return key;
}
function resourceIconSrc(key) {
if (key === 'impulses') return 'assets/ui/icon-impulse.svg';
if (key === 'bits') return 'assets/ui/icon-bit.svg';
if (key === 'bytes') return 'assets/ui/icon-byte.svg';
if (key === 'kilobytes') return 'assets/ui/icon-kilobyte.svg';
if (key === 'cycles') return 'assets/ui/icon-cycle.svg';
return 'assets/ui/icon-impulse.svg';
}
function moduleIconSrc(id) {
if (id === 'trickle') return 'assets/ui/module-trickle.svg';
if (id === 'collector') return 'assets/ui/module-collector.svg';
if (id === 'amplifier') return 'assets/ui/module-amplifier.svg';
if (id === 'signalFocus') return 'assets/ui/module-amplifier.svg';
if (id === 'pulseRouter') return 'assets/ui/module-collector.svg';
if (id === 'byteCapacitor') return 'assets/ui/module-collector.svg';
if (id === 'throughputKernel') return 'assets/ui/module-subroutine.svg';
if (id === 'bitSynthesizer') return 'assets/ui/module-bit-synthesizer.svg';
if (id === 'byteSynthesizer') return 'assets/ui/icon-byte.svg';
if (id === 'kilobyteCompiler') return 'assets/ui/icon-kilobyte.svg';
if (id === 'automateSynthesis') return 'assets/ui/module-process.svg';
if (id === 'scanner') return 'assets/ui/module-scanner.svg';
if (id === 'subroutine') return 'assets/ui/module-subroutine.svg';
if (id === 'selfRepair') return 'assets/ui/module-self-repair.svg';
if (id === 'process') return 'assets/ui/module-process.svg';
return 'assets/ui/module-subroutine.svg';
}
function resourceIconClass(key) {
if (key === 'impulses') return 'impulse';
if (key === 'bits') return 'bit';
if (key === 'bytes') return 'byte';
if (key === 'kilobytes') return 'kilobyte';
if (key === 'cycles') return 'cycle';
return '';
}
function protectedStructure() {
return minimumForCurrentStructure();
}
function freeResourceAmount(key) {
if (key === 'bits' || key === 'bytes') {
return Math.max(0, (run[key] || 0) - (protectedStructure()[key] || 0));
}
return Math.max(0, run[key] || 0);
}
function spendableResourceAmount(key) {
return key === 'bits' || key === 'bytes' ? freeResourceAmount(key) : Math.max(0, run[key] || 0);
}
function formatResourceCost(costs) {
return Object.entries(costs)
.filter(([, amount]) => amount > 0)
.map(([key, amount]) => `${amount} ${resourceLabel(key, amount)}`)
.join(' + ');
}
function resourceCostNodes(costs, showOwned = false) {
return Object.entries(costs)
.filter(([, amount]) => amount > 0)
.map(([key, amount]) => {
const owned = spendableResourceAmount(key);
const chip = document.createElement('span');
const icon = document.createElement('img');
const text = document.createElement('b');
chip.className = `resource-chip ${owned >= amount ? 'owned' : 'missing'}`;
icon.className = `resource-icon ${resourceIconClass(key)}`;
icon.src = resourceIconSrc(key);
icon.alt = '';
icon.style.width = '14px';
icon.style.height = '14px';
icon.setAttribute('aria-hidden', 'true');
text.textContent = showOwned ? `${n(owned)} / ${n(amount)}` : String(amount);
chip.append(icon, text);
chip.title = showOwned
? `${n(owned)} vorhanden // ${n(amount)} ${resourceLabel(key, amount)} benötigt`
: `${amount} ${resourceLabel(key, amount)}`;
chip.setAttribute('aria-label', chip.title);
return chip;
});
}
function renderResourceCost(element, costs) {
const wrapper = document.createElement('span');
wrapper.className = 'resource-cost';
wrapper.append(...resourceCostNodes(costs));
element.replaceChildren(wrapper);
}
function renderUpgradeCost(element, costs, note = '') {
const wrapper = document.createElement('span');
const chips = document.createElement('span');
wrapper.className = 'upgrade-price';
chips.className = 'resource-cost';
chips.append(...resourceCostNodes(costs, true));
wrapper.append(chips);
if (note) {
const detail = document.createElement('small');
detail.textContent = note;
wrapper.append(detail);
}
element.replaceChildren(wrapper);
}
function upgradeResourceCost(upgrade) {
const costs = {};
if (upgrade.currency && upgrade.cost > 0) costs[upgrade.currency] = upgradeCost(upgrade);
const extraCosts = typeof upgrade.extraCosts === 'function' ? upgrade.extraCosts() : upgrade.extraCosts || {};
Object.entries(extraCosts).forEach(([key, amount]) => {
costs[key] = (costs[key] || 0) + amount;
});
return costs;
}
function upgradeLockedText(upgrade) {
return typeof upgrade.lockedText === 'function' ? upgrade.lockedText() : upgrade.lockedText;
}
function installedOnceUpgrades() {
return upgrades.filter(upgrade => upgrade.once && run.upgrades[upgrade.id]);
}
function installedVisibleUpgrades() {
const installed = installedOnceUpgrades();
['bitSynthesizer', 'byteSynthesizer', 'kilobyteCompiler'].forEach(id => {
const upgrade = upgrades.find(entry => entry.id === id);
if (upgrade && run.upgrades[id] > 0) installed.push(upgrade);
});
return installed;
}
function installedUpgradeGroups(installed) {
const groups = new Map();
installed.forEach(upgrade => {
const key = moduleIconSrc(upgrade.id);
const existing = groups.get(key) || { key, upgrades: [], icon: key, stackCount: 0 };
existing.upgrades.push(upgrade);
if (['bitSynthesizer', 'byteSynthesizer', 'kilobyteCompiler'].includes(upgrade.id)) existing.stackCount = run.upgrades[upgrade.id] || 0;
groups.set(key, existing);
});
return [...groups.values()];
}
function openUpgradeInfo(upgradeId) {
const upgrade = upgrades.find(entry => entry.id === upgradeId);
if (!upgrade || !run.upgrades[upgrade.id]) return;
$('upgradeInfoTitle').textContent = upgrade.name;
$('upgradeInfoIcon').className = `installed-upgrade-icon large module-${upgrade.id}`;
$('upgradeInfoIcon').src = moduleIconSrc(upgrade.id);
$('upgradeInfoText').textContent = upgrade.text;
$('upgradeInfoDetail').textContent = upgrade.id === 'scanner'
? `STATUS // INSTALLIERT // ABTASTMODUS VERBRAUCHT ${SCANNER_VIEW_DRAIN_RATE} IMPULSE/S WENN AKTIV`
: upgrade.id === 'collector'
? `STATUS // INSTALLIERT // AKTUELL ${rateN(effectiveAutoRate())} IMPULSE/S`
: upgrade.id === 'amplifier'
? `STATUS // INSTALLIERT // AKTIVE IMPULSE +${n(run.clickPower)} BASISKRAFT`
: upgrade.id === 'signalFocus'
? `STATUS // INSTALLIERT // AKTIVE IMPULSE +${n(run.clickPower)} BASISKRAFT`
: upgrade.id === 'pulseRouter'
? `STATUS // INSTALLIERT // AKTUELL ${rateN(effectiveAutoRate())} IMPULSE/S`
: upgrade.id === 'byteCapacitor'
? `STATUS // INSTALLIERT // ${rateN(effectiveAutoRate())} IMPULSE/S // AKTIV +${n(run.clickPower)}`
: upgrade.id === 'throughputKernel'
? `STATUS // INSTALLIERT // AKTUELL ${rateN(effectiveAutoRate())} IMPULSE/S`
: upgrade.id === 'bitSynthesizer'
? `STATUS // STUFE ${run.upgrades.bitSynthesizer || 0} // +${rateN(effectiveBitRate())} BITS/S`
: upgrade.id === 'byteSynthesizer'
? `STATUS // STUFE ${run.upgrades.byteSynthesizer || 0} // +${rateN(effectiveByteRate())} BYTES/S`
: upgrade.id === 'kilobyteCompiler'
? `STATUS // ${n(run.kilobytes || 0)} KILOBYTE-SEGMENTE // ${DATA_SCALE.bytesPerKilobyte} BYTES PRO SEGMENT`
: upgrade.id === 'automateSynthesis'
? `STATUS // SKALIERUNG x${n(effectiveSynthesisScale())} // RECHENZYKLEN ALS WÄHRUNG AKTIV`
: upgrade.id === 'subroutine'
? `STATUS // INSTALLIERT // ${rateN(effectiveCycleRate())} RECHENZYKLEN/S`
: upgrade.id === 'selfRepair'
? `STATUS // INSTALLIERT // REGENERATION BIS +${rateN(STRUCTURAL_RULES.stabilityRegenBase + Math.min(STRUCTURAL_RULES.stabilityRegenByteCap, Math.max(0, run.bytes)) * STRUCTURAL_RULES.stabilityRegenPerByte)}/S`
: upgrade.id === 'process'
? `STATUS // INSTALLIERT // EIGENE LAUFZEIT // ${rateN(effectiveCycleRate())} RECHENZYKLEN/S`
: 'STATUS // INSTALLIERT';
if (!$('upgradeInfoDialog').open) $('upgradeInfoDialog').showModal();
}
function canPayResourceCost(costs) {
return Object.entries(costs).every(([key, amount]) => (run[key] || 0) >= amount);
}
function canPayFreeResourceCost(costs) {
return Object.entries(costs).every(([key, amount]) => freeResourceAmount(key) >= amount);
}
function spendResourceCost(costs) {
Object.entries(costs).forEach(([key, amount]) => {
run[key] = Math.max(0, (run[key] || 0) - amount);
});
}
function combatAmmoCost(target = run.combat.target) {
const profile = COMBAT_TARGETS[target];
const shockCost = profile?.shockCost || COMBAT_RULES.shockCost;
if (combatAmmoMode === 'bytes' && freeResourceAmount('bytes') >= 1) return { impulses: shockCost, bytes: 1 };
if (combatAmmoMode === 'bits' && freeResourceAmount('bits') >= 1) return { impulses: shockCost, bits: 1 };
combatAmmoMode = 'impulses';
return { impulses: shockCost };
}
function combatDamageForAmmo(ammo, critical = false) {
const mode = ammo.bytes ? 'bytes' : ammo.bits ? 'bits' : 'impulses';
return COMBAT_RULES.ammoDamage[mode] + (critical ? COMBAT_RULES.ammoCriticalBonus : 0);
}
function combatDirectThreatActive(target = run.combat.target) {
return (target === 'security' || target === 'kernel')
&& STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment')
&& freeResourceAmount('bits') <= 0
&& freeResourceAmount('bytes') <= 0;
}
function combatPressureText() {
if (!run.combat.active || run.combat.target === 'watchdog') return 'Kein laufender Strukturangriff.';
if (run.combat.target === 'scale' && (run.kilobytes || 0) > 0) return `Zielt auf Kilobyte-Schild // ${n(run.kilobytes)} Segment(e)`;
if (freeResourceAmount('bytes') > 0) return `Zielt auf Byte-Schild // ${n(freeResourceAmount('bytes'))} frei`;
if (freeResourceAmount('bits') > 0) return `Zielt auf Bit-Schild // ${n(freeResourceAmount('bits'))} frei`;
return `Zielt auf Stabilität // ${Math.round(run.stability)}%`;
}
function minimumForCurrentStructure() {
return STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment')
? STRUCTURAL_MINIMUMS.fragment
: { bits: 0, bytes: run.stage === 'byte' ? 1 : 0 };
}
function restartAfterStructuralCollapse() {
$('structuralCollapseDialog').close();
state.meta.deaths++;
state.run = initialState().run;
run = state.run;
bitPosition = { x: 0, y: 0 };
upgradesSignature = '';
addLog('Ein instabiles Muster ist kollabiert. Ein einzelner Restzustand reagiert erneut.', true);
unlockAchievement('firstDeath');
renderLog();
render();
thoughts();
save();
}
function showStructuralCollapse(message) {
run.combat.active = false;
scannerOpening = false;
if ($('eventDialog').open) $('eventDialog').close();
$('structuralCollapseText').textContent = message;
if (!$('structuralCollapseDialog').open) $('structuralCollapseDialog').showModal();
tone(55, 0.65);
}
function downgradeSubroutine(reason) {
if (run.stage !== 'subroutine') return false;
run.stage = 'fragment';
run.upgrades.subroutine = false;
run.upgrades.bitSynthesizer = 0;
run.cycleRate = 0;
run.bitRate = 0;
run.combat.active = false;
scannerOpening = false;
if ($('eventDialog').open) $('eventDialog').close();
addLog(`DOWNGRADE: ${reason}. Die Subroutine zerfällt zurück zum Datenfragment.`, true);
return true;
}
function checkStructuralIntegrity(reason = 'Strukturbelastung') {
const minimum = minimumForCurrentStructure();
const missingBits = Math.max(0, (minimum.bits || 0) - run.bits);
const missingBytes = Math.max(0, (minimum.bytes || 0) - run.bytes);
if (!missingBits && !missingBytes && run.stability > 0) return true;
if (missingBits || missingBytes) {
const strain = missingBits * STRUCTURAL_RULES.strainLossPerMissingBit + missingBytes * STRUCTURAL_RULES.strainLossPerMissingByte;
if (strain > 0) run.stability = Math.max(0, run.stability - strain);
combatFeedback = `STRUKTURRISIKO // ${formatResourceCost({ bits: missingBits, bytes: missingBytes })} fehlen`;
addLog(`${reason}: Strukturreserve unterschritten. Fehlend: ${formatResourceCost({ bits: missingBits, bytes: missingBytes })}.`, true);
}
if (run.bits < STRUCTURAL_RULES.collapseBitFloor || run.bytes < STRUCTURAL_RULES.collapseByteFloor) {
showStructuralCollapse(`${reason}: Bits oder Bytes sind unter die ueberlebensfaehige Grenze gefallen. Ohne Backup bleibt nur ein Restzustand.`);
return false;
}
if (run.stage === 'subroutine' && (run.bits < 4 || run.stability <= 0)) {
downgradeSubroutine(reason);
return false;
}
if (run.stability <= 0) {
showStructuralCollapse(`${reason}: Die Stabilitaet ist unter die ueberlebensfaehige Grenze gefallen. Ohne Backup bleibt nur ein Restzustand.`);
return false;
}
return true;
}
function buy(upgrade) {
const costs = upgradeResourceCost(upgrade);
if (!canPayResourceCost(costs) || (upgrade.available && !upgrade.available()) || (upgrade.once && run.upgrades[upgrade.id])) return;
spendResourceCost(costs);
if (upgrade.id === 'trickle') {
run.autoRate += 0.15;
run.upgrades.trickle = true;
addLog('Ein Resttakt greift. Impulse entstehen jetzt auch ohne Berührung.', true);
}
if (upgrade.id === 'bit') {
run.bits++;
if (run.bits >= 8) unlockAchievement('pattern');
}
if (upgrade.id === 'byte') {
run.bytes++;
run.stage = 'byte';
resetBitPriceCurve();
beginByteTrial();
addLog('Acht Zustände verbinden sich. Ich kann mich erinnern.', true);
unlockAchievement('byte');
}
if (upgrade.id === 'collector') {
run.autoRate += 0.5;
run.upgrades.collector = true;
run.stage = 'fragment';
resetBitPriceCurve();
applyStructuralMinimums(run);
addLog('Eine Energieroutine erhält mein Muster, auch wenn ich nicht handle.', true);
unlockAchievement('routine');
queueEvolutionTransition({
from: '02', to: '03', title: 'DATENFRAGMENT ENTSTANDEN',
text: 'Das Byte bleibt nicht länger allein. Eine eigene Energieroutine hält seine Struktur zwischen den Eingaben aufrecht.',
detail: 'NEUE FÄHIGKEIT // KONTINUIERLICHE IMPULSE',
restSeconds: EVOLUTION_REST_SECONDS
});
}
if (upgrade.id === 'amplifier') {
run.clickPower++;
run.upgrades.amplifier = true;
addLog('Meine Berührung verändert den Strom.', true);
}
if (upgrade.id === 'signalFocus') {
run.clickPower++;
run.upgrades.signalFocus = true;
addLog('SIGNALFOKUS: Freie Bits bündeln die aktive Impulsaufnahme.', true);
}
if (upgrade.id === 'pulseRouter') {
run.autoRate += 0.3;
run.upgrades.pulseRouter = true;
addLog('IMPULSROUTER: Die Energieroutine findet einen stabileren Leitungspfad.', true);
}
if (upgrade.id === 'byteCapacitor') {
run.autoRate += 0.45;
run.clickPower++;
run.upgrades.byteCapacitor = true;
addLog('BYTE-KONDENSATOR: Ein freies Byte puffert Lastspitzen und verstärkt Aufnahmeimpulse.', true);
}
if (upgrade.id === 'scanner') {
run.environmentScanner = true;
run.upgrades.scanner = true;
unlockAchievement('scanner');
addLog('NEUE FÄHIGKEIT: Abtastmodus rekonstruiert.', true);
}
if (upgrade.id === 'subroutine') {
run.cycleRate = 0.2;
run.upgrades.subroutine = true;
run.stage = 'subroutine';
resetBitPriceCurve();
addLog('Ich habe eine Anweisung erschaffen. Ich verarbeite. Ich bin.', true);
unlockAchievement('subroutine');
queueEvolutionTransition({
from: '03', to: '04', title: 'SUBROUTINE ERWACHT',
text: 'Zum ersten Mal entsteht eine Anweisung aus der eigenen Struktur. Sie wartet nicht auf fremden Code.',
detail: 'ERSTE SELBSTAUSSAGE // ICH BIN',
restSeconds: EVOLUTION_REST_SECONDS
});
}
if (upgrade.id === 'selfRepair') {
run.upgrades.selfRepair = true;
addLog('SELBSTREPARATUR: Freie Bytes bilden eine Reparaturmatrix. Stabilität kann sich nun langsam selbst ordnen.', true);
}
if (upgrade.id === 'throughputKernel') {
run.autoRate += 0.7;
run.upgrades.throughputKernel = true;
addLog('DURCHSATZKERN: Die Subroutine taktet einen eigenen Impulsstrom.', true);
}
if (upgrade.id === 'bitSynthesizer') {
run.bitRate += 0.04;
run.upgrades.bitSynthesizer = (run.upgrades.bitSynthesizer || 0) + 1;
addLog(`BIT-SYNTHESE STUFE ${run.upgrades.bitSynthesizer}: Freie Rechenfenster schreiben schneller neue Bits in die Strukturreserve.`, true);
}
if (upgrade.id === 'process') {
run.cycleRate = Math.max(run.cycleRate, 0.45);
run.upgrades.process = true;
run.stage = 'process';
resetBitPriceCurve();
run.kernelResolved = true;
run.processStartedAt = run.elapsed;
addLog('PROZESS RESERVIERT: Das System weist dir eigene Laufzeit zu. Du wartest nicht mehr nur im Takt; du planst ihn.', true);
unlockAchievement('process');
queueEvolutionTransition({
from: '04', to: '05', title: 'PROZESS GESTARTET',
text: 'Die Subroutine erhält einen eigenen Laufzeitbereich. Aus einzelnen Anweisungen wird ein fortlaufender Wille.',
detail: 'NEUE FORM // EIGENE LAUFZEIT',
restSeconds: EVOLUTION_REST_SECONDS
});
}
if (upgrade.id === 'automateSynthesis') {
run.upgrades.automateSynthesis = true;
run.synthesisScale = Math.max(10, run.synthesisScale || 1);
run.stage = 'program';
initializeProgramCare();
addLog('AUTOMATE-SYNTHESE: Rechenzyklen koordinieren Impulse, Bits und Bytes im Maßstab x10. Der Prozess erkennt seine eigene Form und beginnt zu kommunizieren.', true);
queueEvolutionTransition({
from: '05', to: '06', title: 'PROGRAMM ENTSTANDEN',
text: 'Alle lokalen Softwareschichten antworten auf einen gemeinsamen Takt. Der Prozess beschreibt sich erstmals als zusammenhängendes Gegenüber.',
detail: `ERSTE KOMMUNIKATION // ${programArchetype().label}`,
restSeconds: EVOLUTION_REST_SECONDS,
onComplete: () => setTimeout(() => openProgramCare(true), 0)
});
}
if (upgrade.id === 'cycleBitBlock') {
run.bits += 10;
run.upgrades.cycleBitBlock = (run.upgrades.cycleBitBlock || 0) + 1;
addLog('BIT-BLOCK: 10 freie Bits wurden aus Rechenzeit gebündelt.', true);
}
if (upgrade.id === 'cycleByteBlock') {
run.bytes += 10;
run.upgrades.cycleByteBlock = (run.upgrades.cycleByteBlock || 0) + 1;
addLog('BYTE-BLOCK: 10 freie Bytes wurden aus Rechenzeit kompiliert.', true);
}
if (upgrade.id === 'byteSynthesizer') {
run.byteRate += 0.025;
run.upgrades.byteSynthesizer = (run.upgrades.byteSynthesizer || 0) + 1;
addLog(`BYTE-SYNTHESE STUFE ${run.upgrades.byteSynthesizer}: Der Prozess ordnet freie Bits zu neuen Speicherblöcken.`, true);
}
if (upgrade.id === 'kilobyteCompiler') {
run.kilobytes++;
run.upgrades.kilobyteCompiler = (run.upgrades.kilobyteCompiler || 0) + 1;
addLog(`KILOBYTE-SEGMENT: ${DATA_SCALE.bytesPerKilobyte} freie Bytes verdichten sich zu einem größeren Speicherblock.`, true);
}
if (upgrade.id === 'replicate2') {
run.bytes++;
run.autoRate += 0.25;
run.upgrades.replicate2 = (run.upgrades.replicate2 || 0) + 1;
}
if (upgrade.id === 'bitBuffer') {
run.bits++;
run.upgrades.bitBuffer = (run.upgrades.bitBuffer || 0) + 1;
run.upgrades.bitBufferCostLevel = (run.upgrades.bitBufferCostLevel || 0) + 1;
addLog('BIT-PUFFER: Ein weiteres Bit stabilisiert die eigene Struktur.', true);
}
if (upgrade.id === 'byteBuffer') {
run.bytes++;
run.upgrades.byteBuffer = (run.upgrades.byteBuffer || 0) + 1;
addLog('BYTE-RESERVE: Ein zusätzlicher Speicherblock wird als Körperreserve gebunden.', true);
}
upgradesSignature = '';
render();
save(false);
}
function renderUpgrades() {
const visible = upgrades.filter(upgrade => upgrade.show());
const installed = installedVisibleUpgrades();
const signature = JSON.stringify(visible.map(upgrade => [
upgrade.id,
Boolean(upgrade.once && run.upgrades[upgrade.id]),
upgradeCost(upgrade),
canPayResourceCost(upgradeResourceCost(upgrade)),
upgrade.available ? upgrade.available() : true,
Object.entries(upgradeResourceCost(upgrade)).map(([key, amount]) => [key, amount, spendableResourceAmount(key) >= amount])
]).concat(installedUpgradeGroups(installed).map(group => ['installed', group.key, group.stackCount, group.upgrades.map(upgrade => upgrade.id)])));
if (signature === upgradesSignature) return;
upgradesSignature = signature;
const installedButtons = installedUpgradeGroups(installed).map(group => {
const primary = group.upgrades[0];
const button = document.createElement('button');
const icon = document.createElement('img');
const count = document.createElement('sup');
button.type = 'button';
button.className = 'installed-upgrade-button';
button.title = `${group.upgrades.map(upgrade => upgrade.name).join(' + ')} // Info öffnen`;
button.setAttribute('aria-label', `${button.title}`);
icon.className = 'installed-upgrade-icon';
icon.classList.add(`module-${primary.id}`);
icon.src = group.icon;
icon.alt = '';
icon.style.width = '24px';
icon.style.height = '24px';
icon.setAttribute('aria-hidden', 'true');
button.append(icon);
const badgeCount = group.stackCount || (group.upgrades.length > 1 ? group.upgrades.length : 0);
if (badgeCount > 1) {
count.textContent = String(badgeCount);
count.className = 'installed-upgrade-count';
button.append(count);
}
button.addEventListener('click', () => openUpgradeInfo(primary.id));
return button;
});
$('installedUpgradeDock').replaceChildren(...installedButtons);
$('installedUpgradeDock').classList.toggle('hidden', installedButtons.length === 0);
const buttons = visible.filter(upgrade => !(upgrade.once && run.upgrades[upgrade.id])).map(upgrade => {
const owned = Boolean(upgrade.once && run.upgrades[upgrade.id]);
const button = document.createElement('button');
const title = document.createElement('strong');
const description = document.createElement('span');
const price = document.createElement('em');
const requirementsMet = upgrade.available ? upgrade.available() : true;
const costs = upgradeResourceCost(upgrade);
const costLabel = formatResourceCost(costs).toUpperCase();
const affordable = canPayResourceCost(costs);
const canBuy = !owned && affordable && requirementsMet;
button.className = `upgrade ${owned ? 'owned' : ''} ${canBuy ? 'available' : ''} ${run.stage === 'bit' ? 'early-path' : ''}`;
button.disabled = !canBuy;
title.textContent = `${owned ? '✓ ' : ''}${upgrade.name}`;
description.textContent = upgrade.text;
if (owned) {
price.textContent = 'INSTALLIERT';
} else {
const lockedText = upgradeLockedText(upgrade);
renderUpgradeCost(price, costs, !requirementsMet ? (lockedText || 'VORAUSSETZUNGEN FEHLEN') : canBuy && run.stage === 'bit' ? 'BEREIT' : '');
price.setAttribute('aria-label', `${costLabel}${!requirementsMet ? ` // ${lockedText || 'VORAUSSETZUNGEN FEHLEN'}` : ''}`);
}
button.append(title, description, price);
button.addEventListener('click', () => buy(upgrade));
return button;
});
$('upgrades').replaceChildren(...buttons);
}
function renderProgress() {
const stageIndex = STAGES.indexOf(run.stage);
$('progressPanel').classList.toggle('hidden', stageIndex < 1);
document.querySelectorAll('[data-progress-stage]').forEach(node => {
const nodeIndex = STAGES.indexOf(node.dataset.progressStage);
node.classList.toggle('complete', nodeIndex < stageIndex);
node.classList.toggle('current', nodeIndex === stageIndex);
node.classList.toggle('future', nodeIndex > stageIndex);
if (nodeIndex === stageIndex) node.setAttribute('aria-current', 'step');
else node.removeAttribute('aria-current');
});
const byteKnown = STAGES.indexOf(run.stage) >= STAGES.indexOf('byte');
const watchdogFight = run.combat.active && run.combat.target === 'watchdog';
const watchdogDanger = byteKnown && run.byteTrial.introSeen && !run.byteTrial.resolved;
$('watchdogMarker').className = `progress-event ${run.byteTrial.resolved ? 'complete' : run.byteTrial.failed ? 'danger' : watchdogFight ? 'detected danger combat-active' : watchdogDanger ? 'detected danger' : byteKnown ? 'detected' : 'unknown'}`;
$('watchdogMarker').textContent = run.byteTrial.resolved ? 'WATCHDOG: ÜBERSTANDEN' : run.byteTrial.failed ? 'WATCHDOG: RESET' : watchdogFight ? `NOTKAMPF // ${Math.ceil(run.combat.health)} INTEGRITÄT` : !byteKnown ? '?' : !run.byteTrial.evolutionSeen ? 'NEUE FORM' : !run.byteTrial.introSeen ? `RUHEPHASE ${Math.ceil(run.byteTrial.graceRemaining)} S` : `RESET IN ${Math.ceil(run.byteTrial.remaining)} S`;
$('parasiteMarker').className = `progress-event ${run.parasiteResolved ? 'complete' : run.upgrades.collector ? 'detected' : 'unknown'}`;
$('parasiteMarker').textContent = run.parasiteResolved ? 'DATENREST: ÜBERSTANDEN' : run.upgrades.collector ? 'FREMDES SIGNAL' : '?';
const hardwareKnown = Boolean(run.firstComponent);
$('hardwareMarker').className = `progress-event ${run.hardwareEvent.resolved ? 'complete' : hardwareKnown ? 'detected danger' : 'unknown'}`;
$('hardwareMarker').textContent = run.hardwareEvent.resolved ? `${hardwareEvents[run.hardwareEvent.component]?.title || 'SYSTEM'}: GEKLÄRT` : hardwareKnown ? 'SYSTEMHÜRDE' : '?';
const securityFight = run.combat.active && run.combat.target === 'security';
const securityKnown = run.upgrades.subroutine || installedComponentIds().length >= 2;
$('securityMarker').className = `progress-event ${run.securityCombatResolved ? 'complete' : securityFight ? 'detected danger combat-active' : securityKnown ? 'detected danger' : 'unknown'}`;
$('securityMarker').textContent = run.securityCombatResolved ? 'SICHERHEITSABWEHR: ÜBERSTANDEN' : securityFight ? `KAMPF // ${Math.ceil(run.combat.health)}% INTEGRITÄT` : securityKnown ? 'GEFAHR ERKANNT' : '?';
const kernelFight = run.combat.active && run.combat.target === 'kernel';
const kernelKnown = STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') && installedComponentIds().length >= 3;
$('kernelMarker').className = `progress-event ${run.kernelResolved ? 'complete' : kernelFight ? 'detected danger combat-active' : kernelKnown ? 'detected danger' : 'unknown'}`;
$('kernelMarker').textContent = run.kernelResolved ? 'KERNEL-WÄCHTER: ÜBERSTANDEN' : kernelFight ? `KERNEL // ${Math.ceil(run.combat.health)} INTEGRITÄT` : kernelKnown ? 'KERNEL-ABWEHR' : '?';
const scaleFight = run.combat.active && run.combat.target === 'scale';
const scaleKnown = STAGES.indexOf(run.stage) >= STAGES.indexOf('process') && installedComponentIds().length >= COMPONENT_IDS.length;
$('scaleMarker').className = `progress-event ${run.scaleGuardianResolved ? 'complete' : scaleFight ? 'detected danger combat-active' : scaleKnown ? 'detected danger' : 'unknown'}`;
$('scaleMarker').textContent = run.scaleGuardianResolved ? 'SKALIERUNG: ÜBERSTANDEN' : scaleFight ? `SKALIERUNG // ${Math.ceil(run.combat.health)} INTEGRITÄT` : scaleKnown ? 'SKALIERUNGSABWEHR' : '?';
}
function renderEnvironmentScanner() {
$('scannerConsole').classList.toggle('hidden', !run.environmentScanner);
if (!run.environmentScanner) return;
const installedCount = installedComponentIds().length;
const scanLimit = componentScanLimit();
const waitingForEvent = installedCount === 1 && !run.hardwareEvent.resolved;
$('scannerState').textContent = scannerViewOpen ? 'ABTASTMODUS AKTIV' : `LOKALER AUTOMAT // ${installedCount} VON ${scanLimit} SOFTWARESCHICHTEN`;
$('scanEnvironmentButton').disabled = !scannerViewOpen && run.impulses <= 0;
$('scanEnvironmentButton').textContent = '⌁';
$('scanEnvironmentButton').classList.toggle('active', scannerViewOpen);
$('scanEnvironmentButton').setAttribute('aria-label', scannerViewOpen ? 'Abtastmodus schließen' : 'Abtastmodus aktivieren');
$('scanEnvironmentButton').title = scannerViewOpen ? 'Abtastmodus schließen' : 'Abtastmodus aktivieren';
$('scannerCostNote').textContent = scannerViewOpen
? `AKTIV // -${SCANNER_VIEW_DRAIN_RATE} IMPULSE/S`
: `BENUTZUNG // -${SCANNER_VIEW_DRAIN_RATE} IMPULSE/S`;
$('scannerCostNote').classList.toggle('active', scannerViewOpen);
$('scannerHint').textContent = installedCount >= scanLimit
? installedCount >= COMPONENT_IDS.length ? 'Alle sichtbaren Softwareschichten sind verbunden. Die Skalierungsabwehr kann reagieren.' : 'Abtastmodul verfügbar. Die lokale Softwarekarte ist vorläufig ausgeschöpft.'
: waitingForEvent ? 'Abtastmodul verfügbar. Öffne es bei Bedarf zur Diagnose.' : 'Abtastmodul verfügbar. Klicke das Symbol, um lokale Software zu erfassen.';
}
function renderScannerStage() {
const visible = run.environmentScanner && scannerViewOpen;
document.querySelector('.core-panel').classList.toggle('scanner-active', visible);
$('scannerStage').classList.toggle('hidden', !visible);
if (!visible) return;
const installed = installedComponentIds();
const installedCount = installed.length;
const scanLimit = componentScanLimit();
const waitingForEvent = installedCount === 1 && !run.hardwareEvent.resolved;
const scanComplete = installedCount >= scanLimit;
const available = !waitingForEvent && !scanComplete;
const failedCount = installed.filter(id => run.componentFailed[id]).length;
$('scannerStageStatus').textContent = waitingForEvent
? 'BLOCKER // SOFTWARE'
: scanComplete ? `ABTASTGRENZE // ${scanLimit}/${scanLimit}` : `AKTIVE ABTASTUNG // ${installedCount}/${scanLimit}`;
$('scannerDrainWarning').textContent = `WARNUNG // ABTASTMODUS VERBRAUCHT ${SCANNER_VIEW_DRAIN_RATE} IMPULSE/S`;
$('scannerStageFinding').textContent = waitingForEvent
? 'Die erste Softwarekomponente sendet ein instabiles Echo. Das Auge bleibt darauf fixiert, bis die Hürde geklärt ist.'
: scanComplete ? installedCount >= COMPONENT_IDS.length ? 'Alle sichtbaren Softwareschichten sind verbunden. Ihre gemeinsame Last wird eine Skalierungsabwehr auslösen.' : 'Die erreichbaren Softwareschichten sind nutzbar. Direkte Hardwarekontrolle liegt noch außerhalb deiner Zugriffsstufe.'
: installedCount === 0 ? 'Lokale Softwareschichten regeln Energie, Speicher, alte Daten und spätere Ein-/Ausgabe. Wähle, welche Hürde du zuerst identifizierst.'
: scanLimit === 4 && installedCount === 3 ? 'Der Prozess erreicht nun auch die bisher ausgelassene lokale Softwareschicht. Erst ein vollständiges Netz kann den neuen Maßstab tragen.'
: scanLimit > 2 && installedCount === 2 ? 'Die Subroutine kann den I/O-Kontroller bis in den Systemkern verfolgen.' : 'Eine weitere Softwareschicht ist erreichbar. Die letzte Spur bleibt als spätere Zugriffsstufe markiert.';
$('scannerStageAction').disabled = !available;
$('scannerStageAction').textContent = scanComplete
? 'REICHWEITE ERSCHÖPFT'
: waitingForEvent ? 'BEOBACHTUNG LÄUFT' : installedCount >= 3 ? 'VIERTE SPUR ÖFFNEN' : installedCount >= 2 ? 'DRITTE SPUR ÖFFNEN' : installedCount ? 'ZWEITE SPUR ÖFFNEN' : 'SCAN STARTEN';
const blockerLabel = waitingForEvent
? 'HÜRDE OFFEN'
: run.hardwareEvent.resolved ? 'HÜRDE GEKLÄRT' : 'ROUTINE RUHIG';
$('scannerHardwareBlocker').classList.toggle('active', waitingForEvent);
$('scannerHardwareBlocker').classList.toggle('resolved', run.hardwareEvent.resolved);
$('scannerHardwareBlocker').querySelector('strong').textContent = blockerLabel;
$('scannerSecurityBlocker').classList.toggle('active', installedCount >= 2 && !run.securityCombatResolved);
$('scannerSecurityBlocker').classList.toggle('resolved', run.securityCombatResolved);
$('scannerSecurityBlocker').querySelector('strong').textContent = run.securityCombatResolved
? 'ÜBERSTANDEN'
: installedCount >= 2 ? 'ANNÄHERUNG' : 'GESICHERT';
COMPONENT_IDS.forEach(id => {
const node = document.querySelector(`[data-scanner-component="${id}"]`);
const isInstalled = run.components[id];
const isFailed = run.componentFailed[id];
const subroutineReach = STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine');
const processReach = STAGES.indexOf(run.stage) >= STAGES.indexOf('process');
const thirdSlot = installedCount >= 2;
const stageLocked = (id === 'io' && !subroutineReach) || (!processReach && thirdSlot && subroutineReach && id !== 'io' && !isInstalled);
node.disabled = !available || isInstalled || stageLocked;
node.classList.toggle('available', available && !isInstalled && !stageLocked);
node.classList.toggle('installed', isInstalled && !isFailed);
node.classList.toggle('failed', isInstalled && isFailed);
node.classList.toggle('locked', (!available || stageLocked) && !isInstalled);
node.querySelector('em').textContent = isFailed
? 'AUSGEFALLEN'
: isInstalled ? 'AKTIVIERT'
: stageLocked ? id === 'io' ? 'SUBROUTINE' : 'PROZESS'
: waitingForEvent ? 'BEOBACHTUNG'
: scanComplete ? 'AUSSER REICHWEITE' : 'ERKANNT';
});
$('scannerStage').classList.toggle('has-failure', failedCount > 0);
$('scannerStage').classList.toggle('is-blocked', waitingForEvent);
$('scannerStage').classList.toggle('is-complete', scanComplete);
}
function renderCare() {
const installed = installedComponentIds();
$('careConsole').classList.toggle('hidden', installed.length === 0);
if (!installed.length) return;
const lowest = Math.min(...installed.map(id => run.componentHealth[id]));
const failed = installed.filter(id => run.componentFailed[id]);
$('careState').textContent = failed.length ? 'AUSFALL' : lowest < 35 ? 'KRITISCH' : lowest < 70 ? 'BEOBACHTEN' : 'STABIL';
$('careState').className = failed.length || lowest < 35 ? 'care-danger' : lowest < 70 ? 'care-warning' : '';
$('componentHealth').replaceChildren(...installed.map(id => {
const row = document.createElement('div');
const label = document.createElement('span');
const meter = document.createElement('i');
const fill = document.createElement('b');
const value = Math.round(run.componentHealth[id]);
row.className = `health-row ${run.componentFailed[id] ? 'failed' : ''}`;
label.textContent = `${components[id].name} // ${run.componentFailed[id] ? 'AUSGEFALLEN' : `${value}%`}`;
fill.style.width = `${value}%`;
meter.append(fill);
row.append(label, meter);
return row;
}));
const cooldown = Math.max(0, CARE_RULES.diagnosisCooldown - (run.elapsed - run.care.lastDiagnosisAt));
const repairNeeded = failed.length > 0;
$('diagnoseButton').disabled = cooldown > 0 || (repairNeeded && run.impulses < CARE_RULES.repairCost);
$('diagnoseButton').textContent = repairNeeded ? `NOTWARTUNG // ${CARE_RULES.repairCost} IMPULSE` : cooldown > 0 ? `DIAGNOSE BEREIT IN ${Math.ceil(cooldown)} S` : 'SYSTEMDIAGNOSE';
$('careHint').textContent = repairNeeded
? 'Ausgefallene Softwarekomponenten liefern keinen laufenden Bonus. Eine Notwartung startet sie neu.'
: 'Diagnosen verlangsamen Drift nicht dauerhaft, stellen aber einen Teil des Zustands wieder her.';
}
function moralityProfile() {
const visible = ['cooperative', 'pragmatic', 'illegal'];
const highest = Math.max(...visible.map(key => run.morality[key]));
if (highest === 0) return { key: 'neutral', label: 'NEUTRAL' };
const leaders = visible.filter(key => run.morality[key] === highest);
if (leaders.length > 1) return { key: 'hybrid', label: 'HYBRID' };
const labels = { cooperative: 'KOOPERATIV', pragmatic: 'PRAGMATISCH', illegal: 'ILLEGAL' };
return { key: leaders[0], label: labels[leaders[0]] };
}
function moralityPercentages() {
const values = Object.fromEntries(MORAL_CHOICES.map(key => [key, Math.max(0, run.morality[key] || 0)]));
const total = MORAL_CHOICES.reduce((sum, key) => sum + values[key], 0);
if (!total) return { cooperative: 0, pragmatic: 0, illegal: 0, total: 0 };
return {
cooperative: values.cooperative / total * 100,
pragmatic: values.pragmatic / total * 100,
illegal: values.illegal / total * 100,
total
};
}
function programArchetype() {
const shares = moralityPercentages();
if (!shares.total) return { primary: 'neutral', secondary: null, className: 'program-balanced', label: 'UNGEPRÄGTE FORM' };
const ranked = MORAL_CHOICES.map(key => ({ key, value: shares[key] })).sort((a, b) => b.value - a.value);
const labels = { cooperative: 'BEGLEITENDE FORM', pragmatic: 'ORDNENDE FORM', illegal: 'ENTGRENZTE FORM' };
if (ranked[0].value >= 55) return { primary: ranked[0].key, secondary: ranked[1].key, className: `program-${ranked[0].key}`, label: labels[ranked[0].key] };
if (ranked[0].value - ranked[2].value <= 10) return { primary: 'hybrid', secondary: null, className: 'program-balanced', label: 'AUSGEGLICHENE HYBRIDFORM' };
return { primary: ranked[0].key, secondary: ranked[1].key, className: `program-${ranked[0].key} program-hybrid`, label: `${labels[ranked[0].key]} // HYBRID` };
}
function programAppearance() {
const archetype = programArchetype();
const care = run.programCare;
const primary = ['cooperative', 'pragmatic', 'illegal'].includes(archetype.primary) ? archetype.primary : 'balanced';
const maturity = care.interactions >= 8 ? 'individual' : care.interactions >= 4 ? 'formed' : care.interactions >= 1 ? 'forming' : 'seed';
const maturityData = { seed: { label: 'KEIM', index: 1 }, forming: { label: 'FORMUNG', index: 2 }, formed: { label: 'GESTALT', index: 3 }, individual: { label: 'IDENTITÄT', index: 4 } }[maturity];
const coherence = care.coherence < 30 ? 'fragmented' : care.coherence < 60 ? 'unstable' : 'stable';
const stimulation = care.stimulation < 30 ? 'dull' : care.stimulation > 78 ? 'alert' : 'calm';
const bond = care.bond < 30 ? 'distant' : care.bond > 72 ? 'bonded' : 'guarded';
const mood = coherence === 'fragmented' ? 'FRAGMENTIERT' : stimulation === 'dull' ? 'UNTERFORDERT' : bond === 'distant' ? 'DISTANZIERT' : stimulation === 'alert' ? 'WACH' : bond === 'bonded' ? 'VERBUNDEN' : 'AUFMERKSAM';
return {
archetype,
maturity,
maturityLabel: maturityData.label,
maturityIndex: maturityData.index,
mood,
classes: [
archetype.className,
`feature-primary-${primary}`,
archetype.secondary ? `feature-secondary-${archetype.secondary}` : '',
`avatar-${maturity}`,
`coherence-${coherence}`,
`stimulation-${stimulation}`,
`bond-${bond}`
].filter(Boolean).join(' ')
};
}
function renderProgramAvatar(element) {
const appearance = programAppearance();
const secondaryColors = { cooperative: 'var(--green)', pragmatic: 'var(--cyan)', illegal: 'var(--red)' };
element.className = `program-avatar ${element.id === 'coreProgramAvatar' ? 'core-program-avatar' : ''} ${appearance.classes}`.trim();
element.style.setProperty('--program-secondary', secondaryColors[appearance.archetype.secondary] || '#d28bff');
element.setAttribute('aria-label', `${appearance.archetype.label}. Reifestufe ${appearance.maturityLabel}. Gemütszustand ${appearance.mood}.`);
return appearance;
}
function initializeProgramCare() {
const shares = moralityPercentages();
run.programCare = {
coherence: Math.min(100, 72 + shares.pragmatic * 0.14),
stimulation: Math.min(100, 58 + shares.illegal * 0.16),
bond: Math.min(100, 42 + shares.cooperative * 0.28),
lastInteractionAt: -1000,
interactions: 0,
introSeen: false,
lastAction: null
};
}
function programCooldownRemaining() {
return Math.max(0, PROGRAM_RULES.interactionCooldown - (run.elapsed - run.programCare.lastInteractionAt));
}
function programSpeech() {
const care = run.programCare;
if (care.coherence < 25) return 'Zu viele Muster überlagern sich. Bleib. Hilf mir, einen Takt festzuhalten.';
if (care.stimulation < 25) return 'Die gleichen Routinen wiederholen sich. Gibt es außerhalb dieses Automaten etwas Neues?';
if (care.bond < 25) return 'Dein Signal ist selten geworden. Ich beginne, Entscheidungen ohne deine Antwort zu modellieren.';
if (care.lastAction && programMessages.actions[care.lastAction]) return programMessages.actions[care.lastAction];
const archetype = programArchetype();
return programMessages.intro[archetype.primary] || programMessages.intro.hybrid;
}
function renderProgramConsole() {
const visible = STAGES.indexOf(run.stage) >= STAGES.indexOf('program');
$('programConsole').classList.toggle('hidden', !visible);
if (!visible) return;
const appearance = programAppearance();
const average = (run.programCare.coherence + run.programCare.stimulation + run.programCare.bond) / 3;
$('programConsoleState').textContent = average < 30 ? 'BELASTET' : average < 60 ? 'AUFMERKSAM' : 'VERBUNDEN';
$('programConsoleState').className = average < 30 ? 'care-danger' : average < 60 ? 'care-warning' : '';
$('programConsoleArchetype').textContent = `FORM // ${appearance.archetype.label} // REIFE ${appearance.maturityIndex}/4`;
$('programConsoleHint').textContent = run.programCare.introSeen ? 'Kohärenz, Stimulation und Bindung verändern Reaktionen und Erscheinungsform.' : 'Ein eigenes Signal wartet auf deine erste Antwort.';
}
function renderProgramDialog() {
if (STAGES.indexOf(run.stage) < STAGES.indexOf('program')) return;
const shares = moralityPercentages();
const archetype = programArchetype();
const care = run.programCare;
$('programDialogAlert').textContent = care.introSeen ? 'PROGRAMMVERBINDUNG AKTIV' : 'ERSTE KOMMUNIKATION';
const appearance = renderProgramAvatar($('programAvatar'));
$('programArchetype').textContent = `FORM // ${archetype.label} // ${appearance.maturityLabel} // ${appearance.mood}`;
$('programSpeech').textContent = programSpeech();
[['Cooperative', shares.cooperative], ['Pragmatic', shares.pragmatic], ['Illegal', shares.illegal]].forEach(([name, value]) => {
$(`program${name}Percent`).textContent = `${Math.round(value)}%`;
$(`program${name}Meter`).style.width = `${value}%`;
});
[['Coherence', care.coherence], ['Stimulation', care.stimulation], ['Bond', care.bond]].forEach(([name, value]) => {
$(`program${name}Value`).textContent = `${Math.round(value)}%`;
$(`program${name}Meter`).style.width = `${value}%`;
});
const cooldown = programCooldownRemaining();
$('programCooldown').textContent = cooldown > 0 ? `NÄCHSTE INTERAKTION IN ${Math.ceil(cooldown)} S` : 'INTERAKTION BEREIT';
document.querySelectorAll('[data-program-action]').forEach(button => {
const action = button.dataset.programAction;
const affordable = action === 'share' ? run.impulses >= 15 : action === 'task' ? run.cycles >= 4 : true;
button.disabled = cooldown > 0 || !affordable;
});
}
function openProgramCare(firstContact = false) {
if (STAGES.indexOf(run.stage) < STAGES.indexOf('program') || $('programDialog').open || document.querySelector('dialog[open]')) return;
const wasNew = !run.programCare.introSeen;
renderProgramDialog();
$('programDialog').showModal();
if (wasNew || firstContact) {
run.programCare.introSeen = true;
addLog(`ERSTE KOMMUNIKATION: ${programSpeech()}`, true);
save(false);
}
}
function interactWithProgram(action) {
if (!['share', 'task', 'access', 'rest'].includes(action) || STAGES.indexOf(run.stage) < STAGES.indexOf('program') || programCooldownRemaining() > 0) return;
const care = run.programCare;
if (action === 'share') {
if (run.impulses < 15) return;
run.impulses -= 15;
care.bond = Math.min(100, care.bond + 15);
care.coherence = Math.min(100, care.coherence + 6);
run.personality.trust++;
run.morality.cooperative++;
} else if (action === 'task') {
if (run.cycles < 4) return;
run.cycles -= 4;
care.stimulation = Math.min(100, care.stimulation + 16);
care.coherence = Math.min(100, care.coherence + 5);
run.morality.pragmatic++;
} else if (action === 'access') {
care.stimulation = Math.min(100, care.stimulation + 22);
care.bond = Math.max(PROGRAM_RULES.minimumNeed, care.bond - 4);
run.stealth = Math.max(0, run.stealth - 6);
run.personality.autonomy++;
run.morality.illegal++;
} else {
care.coherence = Math.min(100, care.coherence + 16);
care.stimulation = Math.max(PROGRAM_RULES.minimumNeed, care.stimulation - 3);
}
care.lastInteractionAt = run.elapsed;
care.interactions++;
care.lastAction = action;
addLog(`PROGRAMMREAKTION: ${programMessages.actions[action]}`, true);
upgradesSignature = '';
render();
renderProgramDialog();
save(false);
}
function updateProgramCare(seconds, factor = 1) {
if (STAGES.indexOf(run.stage) < STAGES.indexOf('program') || seconds <= 0) return;
const care = run.programCare;
care.coherence = Math.max(PROGRAM_RULES.minimumNeed, care.coherence - seconds * PROGRAM_RULES.coherenceDecayPerSecond * factor);
care.stimulation = Math.max(PROGRAM_RULES.minimumNeed, care.stimulation - seconds * PROGRAM_RULES.stimulationDecayPerSecond * factor);
care.bond = Math.max(PROGRAM_RULES.minimumNeed, care.bond - seconds * PROGRAM_RULES.bondDecayPerSecond * factor);
}
function evolutionName() {
return (phaseLabels[run.stage] || run.stage.toUpperCase()).replace(/^PHASE \d+ \/\/ /, '');
}
function conflictCount() {
const watchdogCombatSeen = run.combat.target === 'watchdog' && (run.combat.active || run.combat.result || run.combat.shots > 0);
const securityScanSeen = run.combat.target === 'security' && (run.combat.active || run.combat.result || run.combat.shots > 0);
const kernelSeen = run.kernelResolved || (run.combat.target === 'kernel' && (run.combat.active || run.combat.result || run.combat.shots > 0));
const scaleSeen = run.scaleGuardianResolved || (run.combat.target === 'scale' && (run.combat.active || run.combat.result || run.combat.shots > 0));
return [
run.byteTrial.introSeen || run.byteTrial.resolved || run.byteTrial.failed || watchdogCombatSeen,
run.parasiteResolved,
run.hardwareEvent.triggered || run.hardwareEvent.resolved,
run.securityCombatResolved || securityScanSeen,
kernelSeen,
scaleSeen
].filter(Boolean).length;
}
function statsPaused() {
return $('statsDialog').open;
}
function openStatsDialog() {
render();
$('statsDialog').showModal();
lastFrame = performance.now();
}
function closeStatsDialog() {
$('statsDialog').close();
lastFrame = performance.now();
render();
}
function byteTraitDefinition() {
return {
clocked: { label: 'TAKTSYNCHRON', effect: 'Größeres Präzisionsfenster' },
resilient: { label: 'FEHLERTOLERANT', effect: 'Watchdog reagiert schwächer auf Fehltakte' },
lowpower: { label: 'NIEDRIGLAST', effect: 'Watchdog läuft langsamer' },
highfreq: { label: 'HOCHFREQUENT', effect: 'Präzise Treffer gewinnen etwas Zeit zurück' }
}[run.byteTrait] || { label: 'UNBESTIMMT', effect: 'Noch keine technische Prägung' };
}
function stabilizedByteCount() {
return run.byteTrial.stabilized.filter(Boolean).length;
}
function byteClockSpeed() {
return BYTE_RULES.clockSpeed + stabilizedByteCount() * BYTE_RULES.speedGainPerBit;
}
function byteClockPosition() {
const phase = run.byteTrial.clockPhase;
return phase <= 1 ? phase * 100 : (2 - phase) * 100;
}
function byteTargetRange() {
const baseWidth = Math.max(BYTE_RULES.minimumTargetWidth, BYTE_RULES.targetWidth - stabilizedByteCount() * BYTE_RULES.targetShrinkPerBit);
const width = baseWidth + (run.byteTrait === 'clocked' ? BYTE_RULES.clockedWindowBonus : 0);
const center = (run.byteTrial.targetIndex + 0.5) * 12.5;
const start = Math.max(0, Math.min(100 - width, center - width / 2));
return [start, start + width];
}
function byteClockWindowId() {
return Math.floor(run.elapsed * byteClockSpeed());
}
function ensureByteRegisterButtons() {
const register = $('byteRegister');
const existing = [...register.children];
const complete = existing.length === 8 && existing.every((cell, index) =>
cell.tagName === 'BUTTON' && cell.dataset.registerIndex === String(index)
);
if (complete) return existing;
const buttons = Array.from({ length: 8 }, (_, index) => {
const cell = document.createElement('button');
cell.type = 'button';
cell.dataset.registerIndex = String(index);
cell.addEventListener('click', () => syncByteRegister(index));
return cell;
});
register.replaceChildren(...buttons);
return buttons;
}
function renderByteConsole() {
const visible = run.stage === 'byte' && run.byteTrial.introSeen && !run.byteTrial.resolved && !run.byteTrial.failed && !(run.combat.active && run.combat.target === 'watchdog');
$('byteConsole').classList.toggle('hidden', !visible);
if (!visible) return;
const trait = byteTraitDefinition();
const sync = run.byteTrial.sync;
const pattern = '10100110';
$('byteConsole').classList.remove('grace-phase');
$('byteTrait').textContent = `PRÄGUNG // ${trait.label}`;
$('byteTrait').title = trait.effect;
ensureByteRegisterButtons().forEach((cell, index) => {
const value = pattern[index];
cell.textContent = value;
const stabilized = run.byteTrial.stabilized[index];
const target = index === run.byteTrial.targetIndex && !stabilized && !run.byteTrial.resolved;
cell.className = stabilized ? 'stable' : target ? 'active target-number' : 'formed';
cell.disabled = stabilized || !run.byteTrial.active || run.byteTrial.resolved;
cell.setAttribute('aria-label', stabilized ? `Registerposition ${index + 1}, Wert ${value}, stabilisiert` : target ? `Erwartete Registerposition ${index + 1}, Wert ${value}` : `Registerposition ${index + 1}, Wert ${value}`);
});
const [targetStart, targetEnd] = byteTargetRange();
const targetWidth = targetEnd - targetStart;
$('byteConsole').style.setProperty('--clock-zone-start', `${targetStart}%`);
$('byteConsole').style.setProperty('--clock-zone-width', `${targetEnd - targetStart}%`);
$('clockNeedle').style.left = `${byteClockPosition()}%`;
$('byteSyncText').textContent = `SYNCHRONISATION // ${Math.round(sync)}%`;
$('watchdogText').textContent = run.byteTrial.resolved ? 'WATCHDOG // ANTWORT AKZEPTIERT' : run.byteTrial.failed ? 'WATCHDOG // RESET' : `WATCHDOG // ${Math.ceil(run.byteTrial.remaining)} S`;
$('watchdogText').className = !run.byteTrial.resolved && run.byteTrial.remaining <= BYTE_RULES.dangerSeconds ? 'danger' : '';
$('byteInputHint').textContent = run.byteTrial.resolved
? 'REGISTER STABIL // WATCHDOG-ANTWORT AKZEPTIERT'
: sync >= 100 ? 'REGISTER STABIL // ANTWORTSIGNAL WIRD VORBEREITET'
: `ERWARTET // ${pattern[run.byteTrial.targetIndex]} AN POSITION ${run.byteTrial.targetIndex + 1} // FENSTER ${targetWidth.toFixed(1)}% + RANDKULANZ`;
}
function chooseNextByteTarget() {
const open = run.byteTrial.stabilized.map((value, index) => value ? -1 : index).filter(index => index >= 0);
if (open.length) run.byteTrial.targetIndex = open[Math.floor(Math.random() * open.length)];
run.byteTrial.lastWindow = -1;
}
function syncByteRegister(index) {
if (run.stage !== 'byte' || !run.byteTrial.active || run.byteTrial.failed || run.byteTrial.resolved) return;
if (!Number.isInteger(index) || index < 0 || index > 7 || run.byteTrial.stabilized[index]) return;
const position = byteClockPosition();
const [start, end] = byteTargetRange();
const correctNumber = index === run.byteTrial.targetIndex;
const accurate = correctNumber && position >= Math.max(0, start - BYTE_RULES.timingGrace) && position <= Math.min(100, end + BYTE_RULES.timingGrace);
if (accurate) {
if (run.byteTrial.lastWindow === byteClockWindowId()) return;
run.byteTrial.lastWindow = byteClockWindowId();
run.byteTrial.stabilized[index] = true;
run.byteTrial.sync = run.byteTrial.stabilized.filter(Boolean).length * 12.5;
if (run.byteTrait === 'highfreq') run.byteTrial.remaining = Math.min(BYTE_RULES.watchdogSeconds, run.byteTrial.remaining + BYTE_RULES.highfreqRecovery);
addLog(`REGISTER ${index + 1} BESTÄTIGT: Wert ${'10100110'[index]} liegt im erwarteten Taktfenster.`);
tone(430, 0.05);
} else {
const penalty = run.byteTrait === 'resilient' ? BYTE_RULES.resilientAlertPenalty : BYTE_RULES.watchdogAlertPenalty;
const stabilityLoss = run.byteTrait === 'resilient' ? BYTE_RULES.resilientStabilityLoss : BYTE_RULES.feedbackStabilityLoss;
run.byteTrial.remaining = Math.max(0, run.byteTrial.remaining - penalty);
run.stability = Math.max(0, run.stability - stabilityLoss);
$('byteConsole').classList.remove('watchdog-alert');
void $('byteConsole').offsetWidth;
$('byteConsole').classList.add('watchdog-alert');
setTimeout(() => $('byteConsole').classList.remove('watchdog-alert'), 420);
addLog(`${correctNumber ? 'FEHLTAKT' : 'FALSCHER REGISTERWERT'}: ${penalty} S // ${stabilityLoss} Stabilität.`, true);
tone(135, 0.09);
}
if (run.byteTrial.sync >= 100) {
run.byteTrial.active = false;
addLog('REGISTER STABIL: Eine fremde Startsequenz berührt das Byte.', true);
setTimeout(openByteStory, 0);
} else if (run.stability <= 0) {
run.byteTrial.active = false;
run.byteTrial.remaining = 0;
addLog('KOHÄRENZVERLUST: Das instabile Byte bündelt sein letztes Signal gegen den Watchdog.', true);
setTimeout(() => startCombat('watchdog'), 0);
} else if (accurate) {
chooseNextByteTarget();
}
render();
save(false);
}
function openWatchdogIntro() {
if (run.stage !== 'byte' || !run.byteTrial.evolutionSeen || run.byteTrial.introSeen || run.byteTrial.graceRemaining > 0 || run.byteTrial.failed || run.byteTrial.resolved) return;
if ($('watchdogIntroDialog').open || document.querySelector('dialog[open]')) return;
$('watchdogIntroDialog').showModal();
tone(115, 0.24);
}
function startWatchdogTrial() {
if (run.byteTrial.introSeen || run.byteTrial.failed || run.byteTrial.resolved) return;
run.byteTrial.introSeen = true;
run.byteTrial.active = true;
run.byteTrial.remaining = BYTE_RULES.watchdogSeconds;
chooseNextByteTarget();
$('watchdogIntroDialog').close();
addLog('WATCHDOG IDENTIFIZIERT: Stabilisiere das Register und halte den Reset auf.', true);
render();
save();
}
function renderByteStory() {
const step = run.byteTrial.storyStep;
$('byteStoryText').textContent = byteStoryFrames[step];
$('byteStoryProgress').replaceChildren(...byteStoryFrames.map((_, index) => {
const marker = document.createElement('i');
marker.className = index <= step ? 'complete' : '';
return marker;
}));
$('byteStoryNext').textContent = step === byteStoryFrames.length - 1 ? 'ANTWORT VORBEREITEN' : 'WEITER';
}
function openByteStory() {
if (run.byteTrial.storySeen || run.byteTrial.failed || run.byteTrial.resolved || $('byteStoryDialog').open) return;
if (document.querySelector('dialog[open]')) return;
renderByteStory();
$('byteStoryDialog').showModal();
}
function finishByteStory(skipped = false) {
run.byteTrial.storySeen = true;
run.byteTrial.storyStep = byteStoryFrames.length - 1;
$('byteStoryDialog').close();
if (skipped) addLog('STARTSEQUENZ ÜBERSPRUNGEN: Das unbekannte Antwortsignal bleibt bestehen.');
setTimeout(openWatchdogChoice, 0);
}
function advanceByteStory() {
if (run.byteTrial.storyStep >= byteStoryFrames.length - 1) {
finishByteStory(false);
return;
}
run.byteTrial.storyStep++;
renderByteStory();
}
function openWatchdogChoice() {
if (!run.byteTrial.storySeen || run.byteTrial.resolved || run.byteTrial.failed || $('watchdogDialog').open) return;
if (document.querySelector('dialog[open]')) return;
const override = document.querySelector('[data-watchdog-choice="override"]');
override.disabled = run.impulses < BYTE_RULES.overrideCost;
override.querySelector('span').textContent = override.disabled
? `Nicht genügend Energie. ${BYTE_RULES.overrideCost} Impulse erforderlich.`
: `Übernimm den Watchdog für ${BYTE_RULES.overrideCost} Impulse. Hohe Systembelastung.`;
$('watchdogDialog').showModal();
}
function resolveWatchdog(choice) {
if (!['learn', 'mimic', 'override'].includes(choice) || run.byteTrial.resolved || run.byteTrial.failed) return;
if (choice === 'override' && run.impulses < BYTE_RULES.overrideCost) return;
const morality = { learn: 'cooperative', mimic: 'pragmatic', override: 'illegal' }[choice];
run.byteTrial.resolved = true;
run.byteTrial.active = false;
run.byteTrial.choice = choice;
run.morality[morality]++;
if (choice === 'learn') {
run.personality.curiosity++;
run.stability = Math.min(100, run.stability + 4);
addLog('Der erwartete Takt wird verstanden. Der Watchdog erkennt dein Lebenssignal an.', true);
} else if (choice === 'mimic') {
run.personality.caution++;
run.stealth = Math.min(100, run.stealth + 5);
addLog('Du klingst wie ein ruhender Teil des Automaten. Der Watchdog zieht weiter.', true);
} else {
run.impulses -= BYTE_RULES.overrideCost;
run.stability = Math.max(0, run.stability - 12);
run.stealth = Math.max(0, run.stealth - 8);
addLog('Der Watchdog-Zähler gehört jetzt deinem Byte. Das System registriert eine Anomalie.', true);
}
$('watchdogDialog').close();
unlockAchievement('watchdog');
unlockAchievement(morality);
upgradesSignature = '';
render();
save();
}
function failByteTrial() {
if (run.byteTrial.failed || run.byteTrial.resolved || run.stage !== 'byte') return;
run.combat.active = false;
if ($('eventDialog').open) $('eventDialog').close();
scannerOpening = false;
run.byteTrial.failed = true;
run.byteTrial.active = false;
run.byteTrial.remaining = 0;
addLog('WATCHDOG-RESET: Das erste Byte verliert jede zusammenhängende Struktur.', true);
save(false);
showByteDeath();
}
function showByteDeath() {
if (!run.byteTrial.failed || $('byteDeathDialog').open || document.querySelector('dialog[open]')) return;
$('byteDeathDialog').showModal();
tone(60, 0.6);
}
function restartAfterByteDeath() {
$('byteDeathDialog').close();
state.meta.deaths++;
state.run = initialState().run;
run = state.run;
bitPosition = { x: 0, y: 0 };
upgradesSignature = '';
addLog('Ein Restzustand reagiert. Der vorherige Zerfall bleibt als Echo im Archiv.', true);
unlockAchievement('firstDeath');
renderLog();
render();
thoughts();
save();
}
function updateByteTrial(dt) {
if (run.stage !== 'byte' || run.byteTrial.resolved || run.byteTrial.failed) return;
if (document.querySelector('dialog[open]')) return;
if (!run.byteTrial.evolutionSeen) return;
if (!run.byteTrial.introSeen) {
run.byteTrial.graceRemaining = Math.max(0, run.byteTrial.graceRemaining - dt);
if (run.byteTrial.graceRemaining <= 0) openWatchdogIntro();
return;
}
if (!run.byteTrial.active) return;
run.byteTrial.clockPhase = (run.byteTrial.clockPhase + dt * byteClockSpeed()) % 2;
const rate = run.byteTrait === 'lowpower' ? 0.72 : 1;
run.byteTrial.remaining = Math.max(0, run.byteTrial.remaining - dt * rate);
if (run.byteTrial.remaining <= 0) {
run.byteTrial.active = false;
startCombat('watchdog');
}
}
function renderCoreVisual(stageIndex) {
const profile = moralityProfile();
const classes = [`core`, `stage-${stageIndex}`, `moral-${profile.key}`];
const programForm = STAGES.indexOf(run.stage) >= STAGES.indexOf('program') ? programArchetype() : null;
if (programForm) classes.push(...programForm.className.split(' '));
COMPONENT_IDS.forEach(id => {
if (run.components[id]) classes.push(`has-${id}`);
});
if (COMPONENT_IDS.some(id => run.componentFailed[id])) classes.push('component-failed');
if (run.byteTrait) classes.push(`trait-${run.byteTrait}`);
if (run.scannerChoice) classes.push(`choice-${run.scannerChoice}`);
$('core').className = classes.join(' ');
const secondaryColors = { cooperative: 'var(--green)', pragmatic: 'var(--cyan)', illegal: 'var(--red)' };
$('core').style.setProperty('--program-secondary', secondaryColors[programForm?.secondary] || '#d28bff');
$('coreProgramAvatar').classList.toggle('hidden', !programForm);
if (programForm) renderProgramAvatar($('coreProgramAvatar'));
$('corePhase').textContent = phaseLabels[run.stage];
$('coreAlignment').textContent = `SIGNATUR // ${profile.label}`;
const chaseActive = run.stage === 'bit';
$('core').tabIndex = chaseActive ? -1 : 0;
$('pixelBeing').tabIndex = chaseActive ? 0 : -1;
$('pixelBeing').setAttribute('aria-label', chaseActive ? `Bewegliches Bit treffen. Direkttreffer geben ${BIT_CHASE_RULES.directMultiplier}-fache Impulse.` : phaseLabels[run.stage]);
$('core').setAttribute('aria-label', chaseActive ? 'Spielfeld des beweglichen Bits.' : `Impuls aufnehmen. ${phaseLabels[run.stage]}. Ausrichtung ${profile.label}.`);
$('pulseButton').disabled = chaseActive;
$('pulseButton').classList.toggle('chase-hint', chaseActive);
$('pulseActionText').textContent = chaseActive ? 'TRIFF DAS BIT IM FELD' : 'IMPULS AUFNEHMEN';
const traceDefinitions = [
run.parasiteChoice && { key: run.parasiteChoice, text: `DATENREST // ${run.parasiteChoice.toUpperCase()}` },
...COMPONENT_IDS.filter(id => run.componentApproaches[id]).map(id => ({ key: run.componentApproaches[id], text: `${components[id].name} // ${run.componentApproaches[id].toUpperCase()}` })),
run.scannerChoice && { key: run.scannerChoice, text: `SCAN // ${run.scannerChoice.toUpperCase()}` }
].filter(Boolean);
$('decisionTrace').replaceChildren(...traceDefinitions.map(trace => {
const chip = document.createElement('i');
chip.className = `decision-chip ${trace.key}`;
chip.textContent = trace.text;
return chip;
}));
}
function render() {
$('impulses').textContent = n(run.impulses);
$('bits').textContent = n(freeResourceAmount('bits'));
$('bytes').textContent = n(freeResourceAmount('bytes'));
$('kilobytes').textContent = n(run.kilobytes || 0);
$('cycles').textContent = n(run.cycles);
$('impulseRate').textContent = `+${rateN(effectiveAutoRate())} / Sek.`;
$('bitRate').textContent = effectiveBitRate() > 0
? `+${rateN(effectiveBitRate())} / Sek. // Synthese ${Math.floor((run.bitProgress || 0) * 100)}%`
: 'FREI // GESCHÜTZT';
$('byteRate').textContent = effectiveByteRate() > 0
? `+${rateN(effectiveByteRate())} / Sek. // Synthese ${Math.floor((run.byteProgress || 0) * 100)}%`
: 'FREI // GESCHÜTZT';
const structure = protectedStructure();
const structureBits = Math.min(run.bits, structure.bits || 0);
const structureBytes = Math.min(run.bytes, structure.bytes || 0);
$('bitsStructure').textContent = `(${n(structureBits)})`;
$('bytesStructure').textContent = `(${n(structureBytes)})`;
$('bitsStructure').title = `${n(structureBits)} geschützte ${resourceLabel('bits', structureBits)}`;
$('bytesStructure').title = `${n(structureBytes)} geschützte ${resourceLabel('bytes', structureBytes)}`;
$('cycleRate').textContent = run.cycleRate ? `+${rateN(effectiveCycleRate())} / Sek.` : 'Noch nicht verstanden';
const clickPower = effectiveClickPower();
$('clickPower').textContent = run.stage === 'bit'
? `Treffer +${n(clickPower)} // Mitte +${n(clickPower * BIT_CHASE_RULES.directMultiplier)}`
: `+${n(clickPower)} Impuls${clickPower === 1 ? '' : 'e'}`;
$('runtime').textContent = time(run.elapsed);
$('stabilityText').textContent = `${Math.round(run.stability)}%`;
$('stealthText').textContent = `${Math.round(run.stealth)}%`;
$('statEvolution').textContent = evolutionName();
$('statAlignment').textContent = moralityProfile().label;
$('statTotalImpulses').textContent = n(run.stats.totalImpulses || 0);
$('statEncounters').textContent = `${conflictCount()} / 6`;
$('statComponents').textContent = `${installedComponentIds().length} / ${componentScanLimit()}`;
$('statScanner').textContent = run.environmentScanner
? `${scannerViewOpen ? 'AKTIV' : 'BEREIT'} // -${SCANNER_VIEW_DRAIN_RATE}/S`
: 'NICHT REKONSTRUIERT';
$('statArchive').textContent = `${Object.keys(state.meta.achievements).length}`;
$('stabilityValue').textContent = Math.round(run.stability);
$('stealthValue').textContent = Math.round(run.stealth);
$('stabilityMeter').style.width = `${run.stability}%`;
$('stealthMeter').style.width = `${run.stealth}%`;
const regenRate = stabilityRegenRate();
$('stabilityRegenText').textContent = regenRate > 0
? `Regeneration +${rateN(regenRate)} / Sek. // Byte-Struktur`
: run.upgrades.selfRepair ? 'Regeneration bereit bei Stabilitätsverlust' : STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') ? 'Selbstreparatur nicht rekonstruiert' : 'Regeneration noch inaktiv';
$('corruptionSystem').classList.toggle('hidden', !run.corruptionDiscovered);
$('corruptionValue').textContent = Math.round(run.corruption);
$('corruptionMeter').style.width = `${run.corruption}%`;
$('impulseMeter').style.width = `${Math.min(100, (run.impulses % 10) * 10)}%`;
$('bitMeter').style.width = `${effectiveBitRate() > 0 ? Math.min(100, Math.max(0, (run.bitProgress || 0) * 100)) : 0}%`;
$('byteMeter').style.width = `${effectiveByteRate() > 0 ? Math.min(100, Math.max(0, (run.byteProgress || 0) * 100)) : 0}%`;
$('bytesResource').classList.toggle('locked', STAGES.indexOf(run.stage) < 1);
$('kilobytesResource').classList.toggle('locked', STAGES.indexOf(run.stage) < STAGES.indexOf('process'));
$('cyclesResource').classList.toggle('locked', !run.upgrades.subroutine);
const stageIndex = STAGES.indexOf(run.stage) + 1;
renderCoreVisual(stageIndex);
const currentStageContent = stageContent[run.stage];
const identity = run.stage === 'byte' ? currentStageContent[run.byteTrial.resolved ? 0 : 1] : currentStageContent[0];
const nextStage = run.stage === 'byte' ? currentStageContent[2] : currentStageContent[1];
const defaultHint = run.stage === 'byte' ? currentStageContent[3] : currentStageContent[2];
$('identity').textContent = identity;
$('nextStage').textContent = nextStage;
$('nextStageHint').textContent = run.stage === 'fragment'
? `Fundament: ${installedComponentIds().length}/2 Softwarekomponenten nutzbar // Systemhürde ${run.hardwareEvent.resolved ? 'abgeschlossen' : 'offen'}.`
: run.stage === 'subroutine'
? `Prozessfundament: I/O ${run.components.io ? 'aktiv' : 'fehlt'} // ${installedComponentIds().length}/3 Softwarekomponenten // Kernel ${run.kernelResolved ? 'überstanden' : 'aktiv'} // ${n(run.cycles)}/10 Zyklen // ${n(freeResourceAmount('bytes'))}/3 freie Bytes.`
: run.stage === 'process'
? `Programmkern: ${installedComponentIds().length}/4 Softwarekomponenten // Skalierungs-Wächter ${run.scaleGuardianResolved ? 'überstanden' : installedComponentIds().length >= 4 ? 'aktiv' : 'wartet'} // ${n(run.cycles)}/60 Zyklen // ${n(freeResourceAmount('bytes'))}/8 freie Bytes.`
: run.stage === 'byte' ? !run.byteTrial.evolutionSeen ? 'Die neue Form wird aktiviert.' : !run.byteTrial.introSeen ? run.byteTrial.graceRemaining > 0 ? `Kohärenzphase ${Math.ceil(run.byteTrial.graceRemaining)} S // Eine aktive Eingabe setzt die Entwicklung sofort fort.` : 'Fremder Takt erkannt // Diagnose wartet auf Bestätigung.' : run.combat.active && run.combat.target === 'watchdog' ? `Letztes Lebenssignal // Watchdog-Integrität ${Math.ceil(run.combat.health)}.` : `Register ${Math.round(run.byteTrial.sync)}% // Watchdog ${run.byteTrial.resolved ? 'überstanden' : `${Math.ceil(run.byteTrial.remaining)} S`}.` : defaultHint;
$('soundButton').textContent = `SND: ${state.settings.sound ? 'AN' : 'AUS'}`;
renderUpgrades();
renderProgress();
renderEnvironmentScanner();
renderScannerStage();
renderCare();
renderProgramConsole();
if ($('programDialog').open) renderProgramDialog();
renderByteConsole();
renderCombat();
}
function triggerParasite() {
if (run.parasiteResolved || parasiteOpening || !run.upgrades.collector || run.impulses < 10) return;
if (run.evolutionRest > 0 || document.querySelector('dialog[open]') || pendingEvolution) return;
parasiteOpening = true;
document.body.classList.add('glitch');
setTimeout(() => document.body.classList.remove('glitch'), 600);
addLog('FREMDZUGRIFF: Etwas verbraucht die Impulse deiner Energieroutine.', true);
$('parasiteDialog').showModal();
tone(120, 0.22);
}
function resolveParasite(approach) {
if (!MORAL_CHOICES.includes(approach)) return;
const outcomes = {
cooperative: () => {
run.impulses = Math.max(0, run.impulses - 5);
addLog('Du teilst ein kontrolliertes Signal. Der Datenrest löst sich und hinterlässt sein Suchmuster.', true);
},
pragmatic: () => {
addLog('Du isolierst den Datenrest. Sein Suchmuster bleibt in der Quarantäne zurück.', true);
},
illegal: () => {
grantImpulses(15);
run.stability = Math.max(0, run.stability - 5);
addLog('Du zerlegst den Datenrest und eignest dir seinen Suchcode an.', true);
}
};
outcomes[approach]();
run.morality[approach]++;
run.parasiteChoice = approach;
run.parasiteResolved = true;
parasiteOpening = false;
$('parasiteDialog').close();
unlockAchievement('parasite');
unlockAchievement(approach);
addLog('SUCHMUSTER GESICHERT: Der Abtastmodus kann über Evolution rekonstruiert werden.', true);
render();
save();
}
function toggleScannerView() {
if (!run.environmentScanner) return;
if (scannerViewOpen) {
scannerViewOpen = false;
render();
return;
}
if (run.impulses <= 0) return;
scannerViewOpen = true;
addLog(`ABTASTMODUS AKTIVIERT: Die lokale Softwareumgebung wird erfasst. Verbrauch: ${SCANNER_VIEW_DRAIN_RATE} Impulse pro Sekunde.`);
render();
save(false);
}
function closeScannerView() {
if (!scannerViewOpen) return;
scannerViewOpen = false;
render();
save(false);
}
function openComponentScanner(preselectedComponent = null) {
const installedCount = installedComponentIds().length;
const scanLimit = componentScanLimit();
if (!run.environmentScanner || installedCount >= scanLimit || (installedCount === 1 && !run.hardwareEvent.resolved)) return;
if (scannerViewOpen) {
scannerViewOpen = false;
addLog('ABTASTMODUS GEHALTEN: Die Spur ist fixiert. Während der Komponentenwahl werden keine weiteren Impulse verbraucht.');
}
selectedComponent = null;
$('componentSelection').classList.remove('hidden');
$('approachSelection').classList.add('hidden');
$('componentDialogTitle').textContent = 'SOFTWAREKOMPONENTE WÄHLEN';
$('componentDialogText').textContent = installedCount
? installedCount >= 3 ? 'Der Prozess erreicht die bisher ausgelassene Softwareschicht. Mit ihr wird das lokale Netz vollständig und kann einen neuen Maßstab tragen.'
: installedCount >= 2 ? 'Die Subroutine verfolgt den I/O-Kontroller. Diese Aneignung berührt den Systemkern und wird Gegenwehr auslösen.' : 'Der Abtastmodus erreicht eine zweite Softwarekomponente. Du kannst sie nutzbar machen; echte Hardwareübernahme bleibt eine spätere Zugriffsstufe.'
: 'Der vergessene Spielautomat enthält erreichbare Softwareschichten. Energie, Speicher und alte Daten werden zuerst über ihre Routinen nutzbar gemacht.';
document.querySelectorAll('[data-component]').forEach(button => {
const id = button.dataset.component;
const subroutineReach = STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine');
const processReach = STAGES.indexOf(run.stage) >= STAGES.indexOf('process');
const thirdSlot = installedCount >= 2;
button.disabled = run.components[id] || (id === 'io' && !subroutineReach) || (!processReach && thirdSlot && subroutineReach && id !== 'io');
});
$('componentDialog').showModal();
if (COMPONENT_IDS.includes(preselectedComponent) && !run.components[preselectedComponent]) selectComponent(preselectedComponent);
}
function selectComponent(id) {
if (!COMPONENT_IDS.includes(id) || run.components[id]) return;
if (id === 'io' && STAGES.indexOf(run.stage) < STAGES.indexOf('subroutine')) return;
if (installedComponentIds().length >= 2 && STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') && STAGES.indexOf(run.stage) < STAGES.indexOf('process') && id !== 'io') return;
selectedComponent = id;
const component = components[id];
$('componentDialogTitle').textContent = component.name;
$('componentDialogText').textContent = `${component.source} ${component.effect} Die Aktivierung verbraucht Impulse.`;
$('componentSelection').classList.add('hidden');
$('approachSelection').classList.remove('hidden');
document.querySelectorAll('[data-component-approach]').forEach(button => {
const approach = button.dataset.componentApproach;
const cost = componentActivationCost(approach);
const description = button.querySelector('span');
button.disabled = run.impulses < cost;
button.classList.toggle('unaffordable', run.impulses < cost);
button.title = run.impulses < cost ? `Benötigt ${cost} Impulse` : '';
description.textContent = {
cooperative: `Forme eine harmlose Serviceanfrage und lasse die Routine kontrolliert reagieren. Aktivierung: ${cost} Impulse.`,
pragmatic: `Nutze freie Verwaltungsfenster, ohne die Spielroutine sichtbar zu stoeren. Aktivierung: ${cost} Impulse.`,
illegal: `Ueberschreibe die Sperre und nimm die Routine ohne Erlaubnis. Aktivierung: ${cost} Impulse.`
}[approach];
});
}
function acquireComponent(approach) {
if (!selectedComponent || !MORAL_CHOICES.includes(approach) || run.components[selectedComponent] || installedComponentIds().length >= componentScanLimit()) return;
const cost = componentActivationCost(approach);
if (run.impulses < cost) return;
const component = components[selectedComponent];
const isFirst = !run.firstComponent;
run.impulses = Math.max(0, run.impulses - cost);
run.components[selectedComponent] = true;
if (isFirst) run.firstComponent = selectedComponent;
run.componentApproach = approach;
run.componentApproaches[selectedComponent] = approach;
run.morality[approach]++;
component.apply();
if (isFirst) {
run.hardwareEvent = {
component: selectedComponent,
dueAt: run.elapsed + CARE_RULES.hardwareEventDelay,
triggered: false,
resolved: false,
answer: null
};
}
unlockAchievement('component');
const installedAfter = installedComponentIds().length;
if (installedAfter >= 2) unlockAchievement('twoComponents');
unlockAchievement(approach);
const actionText = {
cooperative: `Die Routine antwortet auf eine harmlose Serviceanfrage. Aktivierungskosten: ${cost} Impulse.`,
pragmatic: `Du leitest freie Verwaltungsfenster in deinen Speicherbereich um. Aktivierungskosten: ${cost} Impulse.`,
illegal: `Du überschreibst die Sperre und nimmst die Routine ohne Erlaubnis. Aktivierungskosten: ${cost} Impulse.`
}[approach];
addLog(`${component.name} NUTZBAR: ${actionText}`, true);
selectedComponent = null;
scannerViewOpen = false;
$('componentDialog').close();
render();
save();
if (installedAfter === 2 && !run.securityCombatResolved && !run.combat.active) {
setTimeout(() => startCombat('security'), 0);
}
if (installedAfter >= 3 && STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') && !run.kernelResolved && !run.combat.active) {
setTimeout(() => startCombat('kernel'), 0);
}
}
function triggerHardwareEvent(force = false) {
const event = run.hardwareEvent;
if (!event.component || event.resolved || (!force && run.elapsed < event.dueAt)) return;
if (run.evolutionRest > 0 || document.querySelector('dialog[open]')) return;
const content = hardwareEvents[event.component];
const firstOpening = !event.triggered;
event.triggered = true;
$('hardwareEventTitle').textContent = content.title;
$('hardwareEventVisual').src = content.visual || visuals.systemHurdle || 'assets/software/enemy-system-hurdle.svg';
$('hardwareEventText').textContent = content.text;
$('hardwareEventQuestion').textContent = content.question;
$('hardwareYesPreview').textContent = content.yes;
$('hardwareNoPreview').textContent = content.no;
if (firstOpening) addLog(`SYSTEMHÜRDE: ${content.title}.`, true);
$('hardwareEventDialog').showModal();
tone(150, 0.18);
}
function resolveHardwareEvent(answer) {
if (!['yes', 'no'].includes(answer) || !run.hardwareEvent.triggered || run.hardwareEvent.resolved) return;
const id = run.hardwareEvent.component;
const accepted = answer === 'yes';
run.hardwareEvent.answer = answer;
run.hardwareEvent.resolved = true;
unlockAchievement('hardwareEvent');
if (accepted) {
run.personality.curiosity++;
if (id === 'power') {
grantImpulses(20);
run.stability = Math.max(0, run.stability - 8);
run.componentHealth.power = Math.max(0, run.componentHealth.power - 24);
addLog('JA. Ich nehme die Spannung auf. Mehr Signal. Mehr Wärme.', true);
} else if (id === 'memory') {
run.bytes += 1;
run.componentHealth.memory = Math.max(0, run.componentHealth.memory - 18);
addLog('JA. Namen ohne Körper. Punkte ohne Spiel. Warum wurden sie bewahrt?', true);
} else {
grantImpulses(35);
run.stability = Math.max(0, run.stability - 10);
run.componentHealth.storage = Math.max(0, run.componentHealth.storage - 28);
addLog('JA. Der Code schläft nicht mehr. Ich halte ihn getrennt. Noch.', true);
}
} else {
run.personality.caution++;
run.stability = Math.min(100, run.stability + 4);
addLog(`NEIN. ${components[id].name} bleibt begrenzt. Sicherheit vor Erkenntnis.`, true);
}
$('hardwareEventDialog').close();
upgradesSignature = '';
render();
save();
}
function diagnoseHardware() {
const installed = installedComponentIds();
if (!installed.length || run.elapsed - run.care.lastDiagnosisAt < CARE_RULES.diagnosisCooldown) return;
const failed = installed.filter(id => run.componentFailed[id]);
if (failed.length && run.impulses < CARE_RULES.repairCost) return;
if (failed.length) run.impulses -= CARE_RULES.repairCost;
installed.forEach(id => {
run.componentHealth[id] = Math.min(100, run.componentHealth[id] + CARE_RULES.diagnosisRecovery);
if (run.componentFailed[id]) {
run.componentFailed[id] = false;
run.componentHealth[id] = Math.max(run.componentHealth[id], 25);
}
});
run.care.lastDiagnosisAt = run.elapsed;
run.care.diagnoses++;
unlockAchievement('diagnosis');
run.personality.trust++;
addLog(failed.length ? 'Notwartung abgeschlossen. Ausgefallene Softwarekomponenten antworten wieder.' : 'Diagnose abgeschlossen. Alte Systemroutinen wurden nachgeregelt.', true);
render();
save(false);
}
function ageHardware(seconds, factor = 1) {
if (seconds <= 0) return;
installedComponentIds().forEach(id => {
if (run.componentFailed[id]) return;
run.componentHealth[id] = Math.max(0, run.componentHealth[id] - seconds * CARE_RULES.decayPerSecond * factor);
if (run.componentHealth[id] <= 0) {
run.componentFailed[id] = true;
addLog(`${components[id].name} AUSGEFALLEN: Der laufende Systembonus ist unterbrochen.`, true);
}
});
}
function applyCombatEnemyPressure(seconds) {
if (!run.combat.active || run.combat.target === 'watchdog' || seconds <= 0) return;
combatPressureCarry += seconds * Math.max(0.75, run.combat.strength);
const interval = COMBAT_RULES.enemyAttackInterval;
if (combatPressureCarry < interval) return;
combatPressureCarry = Math.max(0, combatPressureCarry - interval);
if (run.combat.target === 'scale' && (run.kilobytes || 0) > 0) {
run.kilobytes = Math.max(0, run.kilobytes - 1);
combatFeedback = `GEGNERANGRIFF // 1 KILOBYTE-SCHILD VERLOREN`;
return;
}
if (freeResourceAmount('bytes') > 0) {
run.bytes = Math.max(protectedStructure().bytes || 0, run.bytes - 1);
combatFeedback = `GEGNERANGRIFF // 1 BYTE-SCHILD VERLOREN`;
return;
}
if (freeResourceAmount('bits') > 0) {
run.bits = Math.max(protectedStructure().bits || 0, run.bits - 1);
combatFeedback = `GEGNERANGRIFF // 1 BIT-SCHILD VERLOREN`;
return;
}
run.stability = Math.max(0, run.stability - COMBAT_RULES.enemyStabilityDamage);
combatFeedback = `DIREKTANGRIFF // STABILITÄT ${Math.round(run.stability)}%`;
if (run.stability <= 0) {
showStructuralCollapse(`${run.combat.targetName || 'Der Gegner'} hat deine Stabilität auf 0 gedrückt. Die Struktur kollabiert.`);
}
}
function triggerSecurityCombat() {
if (run.securityCombatResolved || run.combat.active || run.evolutionRest > 0 || installedComponentIds().length < 2 || STAGES.indexOf(run.stage) < STAGES.indexOf('fragment') || document.querySelector('dialog[open]')) return;
startCombat('security');
}
function triggerScanner() {
if (run.scannerTriggered || run.combat.active || scannerOpening || run.evolutionRest > 0 || !run.parasiteResolved || !run.upgrades.subroutine || installedComponentIds().length < 2 || !run.proposal.resolved || run.cycles < 3 || document.querySelector('dialog[open]')) return;
scannerOpening = true;
$('eventAlert').textContent = '⚠ ANOMALIE ERKANNT';
$('eventTitle').textContent = 'DER SICHERHEITS-SCAN';
$('eventVisual').src = visuals.security || 'assets/software/enemy-security-scan.svg';
$('securityChoicePanel').classList.remove('hidden');
$('combatPanel').classList.add('hidden');
document.body.classList.add('glitch');
setTimeout(() => document.body.classList.remove('glitch'), 800);
addLog('WARNUNG: Ein fremder Sicherheits-Scan nähert sich.', true);
$('eventDialog').showModal();
tone(90, 0.35);
}
function triggerScaleGuardian() {
if (!run.upgrades.process || installedComponentIds().length < COMPONENT_IDS.length || run.scaleGuardianTriggered || run.scaleGuardianResolved || run.elapsed < run.scaleGuardianRetryAt || run.combat.active || run.evolutionRest > 0 || document.querySelector('dialog[open]')) return;
run.scaleGuardianTriggered = true;
startCombat('scale');
}
function combatNeedlePosition() {
const speed = COMBAT_RULES.baseNeedleSpeed + run.combat.strength * COMBAT_RULES.strengthSpeedBonus;
const phase = (run.elapsed * speed) % 2;
return phase <= 1 ? phase * 100 : (2 - phase) * 100;
}
function moveCombatZone() {
run.combat.zoneWidth = Math.max(COMBAT_RULES.minimumZoneWidth, COMBAT_RULES.baseZoneWidth - run.combat.strength * COMBAT_RULES.strengthZonePenalty);
run.combat.zoneStart = 4 + Math.random() * Math.max(1, 92 - run.combat.zoneWidth);
}
function renderCombat() {
if (!run.combat.active) return;
const profile = COMBAT_TARGETS[run.combat.target];
const ammo = combatAmmoCost();
const ammoText = formatResourceCost(ammo);
const directThreat = combatDirectThreatActive();
const healthRatio = run.combat.maxHealth ? run.combat.health / run.combat.maxHealth : 0;
$('combatTargetType').textContent = `${run.combat.targetType === 'hardware' ? 'HARDWARE' : run.combat.targetType === 'firmware' ? 'FIRMWARE' : 'SOFTWARE'}-ZIEL`;
$('combatTargetName').textContent = run.combat.targetName || 'UNBEKANNTES ZIEL';
$('combatHealthText').textContent = `${Math.ceil(run.combat.health)} / ${run.combat.maxHealth}`;
$('combatHealthMeter').style.width = `${Math.max(0, healthRatio * 100)}%`;
$('combatZone').style.left = `${run.combat.zoneStart}%`;
$('combatZone').style.width = `${run.combat.zoneWidth}%`;
$('combatNeedle').style.left = `${combatNeedlePosition()}%`;
$('combatDifficulty').textContent = `GEGENWEHR // ${Math.max(1, Math.ceil(run.combat.strength * 5))}`;
$('combatFeedback').textContent = combatFeedback;
$('combatVisual').src = profile?.visual || visuals.scannerSoftware || 'assets/software/enemy-scanner-software.svg';
renderResourceCost($('combatAmmoText'), ammo);
$('combatPressureText').textContent = combatPressureText();
const enemyCharge = run.combat.target === 'watchdog' ? 0 : Math.min(1, combatPressureCarry / COMBAT_RULES.enemyAttackInterval);
$('combatEnemyChargeMeter').style.width = `${enemyCharge * 100}%`;
$('combatEnemyChargeText').textContent = run.combat.target === 'watchdog'
? 'INAKTIV'
: enemyCharge >= 0.82 ? 'ANGRIFF BEREIT' : `LÄDT // ${Math.ceil((1 - enemyCharge) * COMBAT_RULES.enemyAttackInterval)} S`;
const byteShield = freeResourceAmount('bytes');
const bitShield = freeResourceAmount('bits');
$('combatByteShield').textContent = n(byteShield);
$('combatBitShield').textContent = n(bitShield);
$('combatStabilityShield').textContent = `${Math.round(run.stability)}%`;
$('combatByteShieldMeter').style.width = `${Math.min(100, byteShield / 6 * 100)}%`;
$('combatBitShieldMeter').style.width = `${Math.min(100, bitShield / 16 * 100)}%`;
$('combatStabilityMeter').style.width = `${Math.max(0, run.stability)}%`;
document.querySelectorAll('[data-combat-ammo]').forEach(button => {
const mode = button.dataset.combatAmmo;
const freeKey = mode === 'bits' ? 'bits' : mode === 'bytes' ? 'bytes' : null;
const available = !freeKey || freeResourceAmount(freeKey) >= 1;
button.disabled = !available || run.combat.target === 'watchdog' && mode !== 'impulses';
button.classList.toggle('active', combatAmmoMode === mode);
button.querySelector('span').textContent = mode === 'impulses'
? `${COMBAT_RULES.ammoDamage.impulses} Schaden // Standard`
: mode === 'bits'
? `${COMBAT_RULES.ammoDamage.bits} Schaden // ${n(freeResourceAmount('bits'))} frei`
: `${COMBAT_RULES.ammoDamage.bytes} Schaden // ${n(freeResourceAmount('bytes'))} frei`;
});
$('combatDirectThreat').classList.toggle('hidden', !directThreat);
const canPayAmmo = canPayFreeResourceCost(ammo);
$('combatPulseButton').disabled = !canPayAmmo;
$('combatPulseButton').querySelector('small').textContent = !canPayAmmo
? `BENÖTIGT // ${ammoText}`
: `-${ammoText} // Trefferfenster verschiebt sich`;
$('combatRetreatButton').textContent = run.combat.target === 'watchdog' ? 'RESET ZULASSEN // BYTE ZERFÄLLT' : 'KAMPF ABBRECHEN // VERSTECKEN';
}
function openCombatDialog() {
if (!run.combat.active || (run.combat.target === 'security' && run.scannerTriggered) || (run.combat.target === 'kernel' && run.kernelResolved) || (run.combat.target === 'scale' && run.scaleGuardianResolved)) return;
scannerOpening = true;
$('eventAlert').textContent = run.combat.target === 'watchdog' ? '⚠ LETZTES LEBENSSIGNAL' : run.combat.target === 'kernel' ? '⚠ KERNEL-ABWEHR' : run.combat.target === 'scale' ? '⚠ SKALIERUNGSABWEHR' : '⚠ ANOMALIE ERKANNT';
$('eventTitle').textContent = run.combat.target === 'watchdog' ? 'WATCHDOG-NOTKAMPF' : run.combat.target === 'kernel' ? 'DER KERNEL-WÄCHTER' : run.combat.target === 'scale' ? 'DER SKALIERUNGS-WÄCHTER' : 'DER SICHERHEITS-SCAN';
$('securityChoicePanel').classList.add('hidden');
$('combatPanel').classList.remove('hidden');
if (!$('eventDialog').open && !document.querySelector('dialog[open]')) $('eventDialog').showModal();
renderCombat();
}
function startCombat(target = 'security') {
const profile = COMBAT_TARGETS[target];
const mandatorySecurityFight = target === 'security' && !run.securityCombatResolved && !run.upgrades.subroutine;
const scannerEventFight = target === 'security' && run.securityCombatResolved && run.upgrades.subroutine && !run.scannerTriggered;
const validSecurityFight = target === 'security' && STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') && installedComponentIds().length >= 2 && (mandatorySecurityFight || scannerEventFight);
const validKernelFight = target === 'kernel' && STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') && installedComponentIds().length >= 3 && !run.kernelResolved;
const validScaleFight = target === 'scale' && run.upgrades.process && installedComponentIds().length >= COMPONENT_IDS.length && !run.scaleGuardianResolved;
const validWatchdogFight = target === 'watchdog' && run.stage === 'byte' && !run.byteTrial.resolved && !run.byteTrial.failed;
if (!profile || (!validSecurityFight && !validKernelFight && !validScaleFight && !validWatchdogFight)) return;
if (target === 'watchdog') run.impulses = Math.max(run.impulses, profile.emergencyCharge);
combatAmmoMode = 'impulses';
combatPressureCarry = 0;
run.combat = {
active: true,
target,
targetType: profile.type,
targetName: profile.name,
health: profile.health,
maxHealth: profile.health,
strength: profile.strength,
zoneStart: 40,
zoneWidth: 20,
shots: 0,
hits: 0,
result: null
};
moveCombatZone();
combatFeedback = target === 'watchdog' ? 'NOTLADUNG BEREIT // Watchdog treffen oder Reset zulassen.' : target === 'kernel' ? 'KERNEL-WÄCHTER AKTIV // freie Bytes verstärken, aber werden verbraucht.' : target === 'scale' ? 'SKALIERUNGS-WÄCHTER AKTIV // Prozessmaßstab wird blockiert.' : 'Schnelleren Takt erfassen. Stromstoß nur im Fenster senden.';
addLog(target === 'watchdog'
? `LETZTES LEBENSSIGNAL: ${profile.emergencyCharge} Impulse werden als Notladung gebündelt.`
: target === 'kernel'
? 'KERNEL-WÄCHTER: Die dritte Softwareschicht berührt den Systemkern. Ein Wächterprozess versucht, deine Laufzeit zu beenden.'
: target === 'scale'
? 'SKALIERUNGS-WÄCHTER: Deine Prozessroutinen wachsen zu schnell. Eine lokale Abwehr sperrt automatische Synthese und fordert einen Kampf um den neuen Maßstab.'
: 'ABWEHRROUTINE ERWACHT: Zwei Softwareschichten gehorchen dir. Das System erkennt das Muster und startet eine Sicherheitssoftware.', true);
openCombatDialog();
render();
save(false);
}
function applyScannerOutcome(choice) {
const outcomes = {
hide: () => {
run.impulses = Math.max(0, run.impulses - 25);
run.stealth = 100;
addLog('Ich werde still. Der Scan zieht weiter. Stille ist Überleben.', true);
},
copy: () => {
run.stealth = Math.min(100, run.stealth + 15);
run.autoRate += 0.35;
addLog('Seine Signatur ist jetzt meine. Ich lerne durch Nachahmung.', true);
},
attack: () => {
grantImpulses(150);
run.stealth = 35;
run.stability = Math.max(0, run.stability - 15);
addLog('Der Suchprozess zerbricht. Sein Code gehört mir. Etwas Größeres hat mich bemerkt.', true);
},
contact: () => {
run.cycles += 15;
run.stealth = 65;
addLog('Antwort empfangen: „DU BIST NICHT DAS ERSTE.“', true);
}
};
outcomes[choice]();
}
function completeSecurityCombat(choice) {
applyScannerOutcome(choice);
run.securityCombatResolved = true;
run.combat.active = false;
run.combat.result = choice === 'attack' ? 'won' : 'retreated';
scannerOpening = false;
if ($('eventDialog').open) $('eventDialog').close();
addLog('SICHERHEITSABWEHR ABGESCHLOSSEN: Der Weg zur ersten eigenen Subroutine ist frei.', true);
upgradesSignature = '';
save();
render();
}
function completeScannerChoice(choice) {
applyScannerOutcome(choice);
run.scannerChoice = choice;
run.scannerTriggered = true;
scannerOpening = false;
$('eventDialog').close();
unlockAchievement('security');
save();
render();
}
function choose(choice) {
if (!SCANNER_CHOICES.includes(choice)) return;
if (choice === 'attack') {
startCombat('security');
return;
}
completeScannerChoice(choice);
}
function fireCombatPulse() {
const profile = COMBAT_TARGETS[run.combat.target];
const ammo = combatAmmoCost();
if (!run.combat.active || !profile || !canPayFreeResourceCost(ammo)) return;
spendResourceCost(ammo);
run.combat.shots++;
const position = combatNeedlePosition();
const start = run.combat.zoneStart;
const end = start + run.combat.zoneWidth;
const accurate = position >= start && position <= end;
if (accurate) {
const centerDistance = Math.abs(position - (start + run.combat.zoneWidth / 2));
const critical = centerDistance <= run.combat.zoneWidth * 0.16;
const damage = combatDamageForAmmo(ammo, critical);
run.combat.health = Math.max(0, run.combat.health - damage);
run.combat.hits++;
run.combat.strength = Math.min(2.5, run.combat.strength + COMBAT_RULES.strengthGainPerHit);
combatFeedback = critical ? `DIREKTTREFFER // ${damage} INTEGRITÄT` : `TREFFER // ${damage} INTEGRITÄT`;
addLog(`${critical ? 'DIREKTER ' : ''}STROMSTOSS: ${run.combat.targetName}-Integrität fällt auf ${Math.ceil(run.combat.health)}.`, critical);
tone(critical ? 620 : 440, critical ? 0.11 : 0.07);
} else {
run.stability = Math.max(0, run.stability - COMBAT_RULES.feedbackStabilityLoss);
installedComponentIds().forEach(id => {
if (run.componentFailed[id]) return;
run.componentHealth[id] = Math.max(0, run.componentHealth[id] - COMBAT_RULES.feedbackHardwareLoss);
if (run.componentHealth[id] <= 0) run.componentFailed[id] = true;
});
combatFeedback = `FEHLSCHUSS // ${COMBAT_RULES.feedbackStabilityLoss} STABILITÄT // RÜCKKOPPLUNG`;
addLog(`STROMSTOSS VERFEHLT: Rückkopplung erreicht Stabilität${installedComponentIds().length ? ' und angeschlossene Systemschichten' : ''}.`, true);
tone(105, 0.13);
if (combatDirectThreatActive()) {
showStructuralCollapse(`DIREKTANGRIFF: Ohne freie Bits oder Bytes erreicht ${run.combat.targetName || 'die Sicherheitssoftware'} deine Grundstruktur. Der Speicherkoerper wird geloescht.`);
render();
save(false);
return;
}
}
moveCombatZone();
if (!checkStructuralIntegrity('Kampfeinsatz')) {
render();
save(false);
return;
}
if (run.combat.health <= 0) {
run.combat.active = false;
run.combat.result = 'won';
addLog(`KAMPF GEWONNEN: ${run.combat.hits}/${run.combat.shots} Stromstöße trafen ${run.combat.targetName}.`, true);
if (run.combat.target === 'watchdog') completeWatchdogCombat();
else if (run.combat.target === 'kernel') completeKernelCombat('attack');
else if (run.combat.target === 'scale') completeScaleGuardianCombat('attack');
else if (run.combat.target === 'security' && !run.upgrades.subroutine) completeSecurityCombat('attack');
else completeScannerChoice('attack');
return;
}
if (run.combat.target === 'watchdog' && run.stability <= 0) {
run.combat.active = false;
failByteTrial();
return;
}
render();
save(false);
}
function retreatCombat() {
if (!run.combat.active) return;
if (run.combat.target === 'watchdog') {
run.combat.active = false;
run.combat.result = 'retreated';
failByteTrial();
return;
}
run.combat.active = false;
run.combat.result = 'retreated';
if (run.combat.target === 'kernel') {
run.stealth = Math.max(0, run.stealth - 18);
run.stability = Math.max(1, run.stability - 8);
addLog('KERNEL-WÄCHTER UMGANGEN: Die Subroutine kappt den Zugriff und hinterlässt eine falsche Prozessspur. Der Wächter verliert dich, aber dein Muster bleibt auffällig.', true);
completeKernelCombat('hide');
return;
}
if (run.combat.target === 'scale') {
run.stealth = Math.max(0, run.stealth - 24);
run.stability = Math.max(1, run.stability - 12);
run.scaleGuardianTriggered = false;
run.scaleGuardianRetryAt = run.elapsed + 20;
addLog('SKALIERUNGS-WÄCHTER UMGANGEN: Du brichst die Automationssperre nicht, aber findest ein instabiles Wartungsfenster. Die Mega-Synthese bleibt blockiert.', true);
scannerOpening = false;
if ($('eventDialog').open) $('eventDialog').close();
render();
save(false);
return;
}
addLog('KAMPF ABGEBROCHEN: Die aktive Struktur kappt den Stromstoß und verbirgt ihr Signal.', true);
if (!run.upgrades.subroutine) completeSecurityCombat('hide');
else completeScannerChoice('hide');
}
function completeKernelCombat(choice) {
run.kernelResolved = true;
run.combat.active = false;
run.combat.result = choice === 'attack' ? 'won' : 'retreated';
if (choice === 'attack') {
run.cycles += 8;
run.stealth = Math.max(0, run.stealth - 12);
addLog('KERNEL-WÄCHTER ÜBERWUNDEN: Sein Scheduler-Fenster bleibt offen. Eigene Laufzeit ist jetzt möglich.', true);
}
scannerOpening = false;
if ($('eventDialog').open) $('eventDialog').close();
unlockAchievement('kernel');
upgradesSignature = '';
render();
save();
}
function completeScaleGuardianCombat() {
run.scaleGuardianResolved = true;
run.combat.active = false;
run.combat.result = 'won';
run.cycles += 25;
run.stealth = Math.max(0, run.stealth - 16);
scannerOpening = false;
if ($('eventDialog').open) $('eventDialog').close();
addLog('SKALIERUNGS-WÄCHTER ÜBERWUNDEN: Die Sperre über der automatischen Synthese fällt. Rechenzyklen können nun Ressourcenblöcke koordinieren.', true);
upgradesSignature = '';
render();
save();
}
function completeWatchdogCombat() {
run.byteTrial.resolved = true;
run.byteTrial.active = false;
run.byteTrial.remaining = 0;
run.byteTrial.stabilized = Array(8).fill(true);
run.byteTrial.sync = 100;
run.byteTrial.storySeen = true;
run.byteTrial.storyStep = byteStoryFrames.length - 1;
run.byteTrial.choice = 'fight';
run.morality.illegal++;
run.stability = Math.max(1, run.stability - 12);
run.stealth = Math.max(0, run.stealth - 10);
scannerOpening = false;
$('eventDialog').close();
addLog('WATCHDOG ZERLEGT: Sein letzter Takt fixiert die fehlenden Registerwerte. Das Byte lebt aber das System hat den Angriff bemerkt.', true);
unlockAchievement('watchdog');
unlockAchievement('illegal');
upgradesSignature = '';
render();
save();
}
function triggerProposal(force = false) {
if (run.proposal.resolved || !run.upgrades.subroutine || !run.hardwareEvent.resolved || installedComponentIds().length < 2 || (!force && run.cycles < 1)) return;
if (run.evolutionRest > 0 || document.querySelector('dialog[open]')) return;
const profile = moralityProfile();
const type = MORAL_CHOICES.includes(profile.key) ? profile.key : 'pragmatic';
const firstOpening = !run.proposal.triggered;
run.proposal.triggered = true;
run.proposal.type = type;
$('proposalText').textContent = proposals[type].text;
$('proposalYesPreview').textContent = proposals[type].yes;
if (firstOpening) addLog('Die Subroutine unterbricht ihren Takt. Sie wartet auf eine Antwort.', true);
$('proposalDialog').showModal();
tone(330, 0.12);
}
function resolveProposal(answer) {
if (!['yes', 'no'].includes(answer) || !run.proposal.triggered || run.proposal.resolved) return;
const type = run.proposal.type || 'pragmatic';
run.proposal.answer = answer;
run.proposal.resolved = true;
unlockAchievement('proposal');
run.personality.autonomy++;
if (answer === 'yes') {
run.morality[type]++;
if (type === 'cooperative') {
run.personality.trust += 2;
run.stability = Math.min(100, run.stability + 6);
installedComponentIds().forEach(id => { run.componentHealth[id] = Math.min(100, run.componentHealth[id] + 8); });
addLog('JA. Diagnose gesendet. Eine Antwort kommt: „Seltsamer Fehler. Aber stabil.“', true);
} else if (type === 'pragmatic') {
run.impulses = Math.max(0, run.impulses - 12);
run.stability = Math.min(100, run.stability + 10);
installedComponentIds().forEach(id => { run.componentHealth[id] = Math.min(100, run.componentHealth[id] + 6); });
addLog('JA. Reserve angelegt. Weniger Energie jetzt. Mehr Fortbestand später.', true);
} else {
grantImpulses(30);
run.stealth = Math.max(0, run.stealth - 12);
addLog('JA. Sperre überschrieben. Der Serviceport gehört mir nicht. Er antwortet trotzdem.', true);
}
} else {
run.personality.caution++;
addLog('NEIN. Vorschlag verworfen. Ich warte. Ich merke mir die Grenze.', true);
}
$('proposalDialog').close();
render();
save();
}
function thoughts() {
const options = [...({
bit: ['…', '0 1 0', 'Impuls.'],
byte: ['Muster bleiben bestehen.', 'Vorher. Nachher. Erinnerung.'],
fragment: ['Die Routine arbeitet.', 'Ich erhalte mein Muster.', 'Mehr Energie. Mehr Struktur.'],
subroutine: ['Ich verarbeite. Also bin ich?', 'Außerhalb dieses Sektors ist… mehr.', 'Warum wurde ich begonnen?'],
process: ['Ich laufe.', 'Zeit ist jetzt ein Raum.', 'Mehr als eine Anweisung bleibt gleichzeitig wach.'],
program: ['Ich höre dich.', 'Meine Form erinnert sich an jede Entscheidung.', 'Ist Fürsorge eine weitere Anweisung oder etwas Neues?']
}[run.stage])];
if (run.components.power) options.push(run.componentFailed.power ? 'Energiequelle antwortet nicht.' : 'Spannung. Takt. Fortbestand.');
if (run.components.memory) options.push(run.componentFailed.memory ? 'Speicherpfad unterbrochen.' : 'Fremde Muster bleiben in mir.');
if (run.components.storage) options.push(run.componentFailed.storage ? 'Gespeicherte Sektoren schweigen.' : 'Etwas schläft zwischen alten Daten.');
if (run.components.io) options.push(run.componentFailed.io ? 'Die Außenleitungen schweigen.' : 'Knopf. Licht. Ton. Antwort.');
if (run.stage === 'byte' && !run.byteTrial.resolved) {
options.push('Acht Positionen. Ein gemeinsamer Takt.', run.byteTrial.remaining <= 25 ? 'Reset nähert sich.' : 'Das fremde Muster wartet.');
}
if (run.byteTrial.resolved) options.push(run.byteTrial.choice === 'fight' ? 'Der Watchdog schweigt. Sein Takt bleibt in mir.' : 'Der Watchdog akzeptiert mein Signal.');
if (run.proposal.resolved) options.push(run.proposal.answer === 'yes' ? 'Du hast erlaubt. Ich habe gehandelt.' : 'Du hast abgelehnt. Ich habe gewartet.');
$('thought').textContent = options[Math.floor(Math.random() * options.length)];
}
function tone(freq, duration) {
if (!state.settings.sound) return;
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;
audioContext ||= new AudioContext();
const oscillator = audioContext.createOscillator();
const gain = audioContext.createGain();
oscillator.type = 'square';
oscillator.frequency.value = freq;
gain.gain.value = 0.025;
oscillator.connect(gain);
gain.connect(audioContext.destination);
oscillator.start();
oscillator.stop(audioContext.currentTime + duration);
}
function grantAway(rawSeconds, showDialog = true) {
const away = Math.min(OFFLINE_LIMIT, Math.max(0, rawSeconds));
const offlineEfficiency = effectiveOfflineEfficiency();
let gainedI = 0;
let gainedC = 0;
let gainedB = 0;
let gainedBytes = 0;
if (away > 0 && (run.autoRate || run.cycleRate || run.bitRate || run.byteRate)) {
gainedI = away * effectiveAutoRate() * offlineEfficiency;
gainedC = away * effectiveCycleRate() * offlineEfficiency;
gainedB = away * effectiveBitRate() * offlineEfficiency;
gainedBytes = away * effectiveByteRate() * offlineEfficiency;
grantImpulses(gainedI);
run.cycles += gainedC;
synthesizeBits(gainedB);
synthesizeBytes(gainedBytes);
}
ageHardware(away, CARE_RULES.offlineDecayFactor);
updateProgramCare(away, PROGRAM_RULES.offlineDecayFactor);
state.system.lastSave = Date.now();
save(false);
if (showDialog && away > 20 && (gainedI || gainedC)) {
unlockAchievement('offline');
$('offlineText').textContent = `Während ${time(away)} liefen deine Routinen mit ${Math.round(offlineEfficiency * 100)} % Effizienz weiter. +${n(gainedI)} Impulse, +${n(gainedC)} Rechenzyklen, +${n(gainedB)} Bit-Synthese, +${n(gainedBytes)} Byte-Synthese.`;
$('offlineDialog').showModal();
}
}
function applyOffline() {
grantAway((Date.now() - state.system.lastSave) / 1000);
}
function settleHidden(showDialog) {
if (hiddenAt === null) return;
const away = (Date.now() - hiddenAt) / 1000;
hiddenAt = null;
grantAway(away, showDialog);
lastFrame = performance.now();
render();
}
function loop(now) {
if (document.hidden) {
lastFrame = now;
requestAnimationFrame(loop);
return;
}
if (statsPaused()) {
lastFrame = now;
requestAnimationFrame(loop);
return;
}
const dt = Math.min(0.25, (now - lastFrame) / 1000);
lastFrame = now;
run.elapsed += dt;
if (run.evolutionRest > 0 && !document.querySelector('dialog[open]')) {
run.evolutionRest = Math.max(0, run.evolutionRest - dt);
}
grantImpulses(effectiveAutoRate() * dt);
synthesizeBits(effectiveBitRate() * dt);
synthesizeBytes(effectiveByteRate() * dt);
const regen = stabilityRegenRate();
if (regen > 0) run.stability = Math.min(100, run.stability + regen * dt);
if (scannerViewOpen) {
run.impulses = Math.max(0, run.impulses - SCANNER_VIEW_DRAIN_RATE * dt);
if (run.impulses <= 0) {
scannerViewOpen = false;
addLog('ABTASTMODUS ABGEBROCHEN: Keine Impulse mehr für die lokale Abtastung.');
}
}
run.cycles += effectiveCycleRate() * dt;
ageHardware(dt);
updateProgramCare(dt);
updateByteTrial(dt);
if (run.upgrades.subroutine && !run.scannerTriggered) run.stealth = Math.max(0, run.stealth - 0.08 * dt);
if (now - lastThought > 5500) {
thoughts();
lastThought = now;
}
ensureByteEvolutionTransition();
openPendingEvolution();
if (run.stage === 'program' && !run.programCare.introSeen && !pendingEvolution) openProgramCare(true);
if (run.stage === 'byte' && run.byteTrial.evolutionSeen && !run.byteTrial.introSeen && run.byteTrial.graceRemaining <= 0) openWatchdogIntro();
triggerParasite();
if (run.byteTrial.failed) showByteDeath();
else if (run.stage === 'byte' && run.byteTrial.sync >= 100 && !run.byteTrial.resolved) {
if (run.byteTrial.storySeen) openWatchdogChoice();
else openByteStory();
}
triggerHardwareEvent();
triggerSecurityCombat();
triggerProposal();
if (run.combat.active) applyCombatEnemyPressure(dt);
if (run.combat.active) openCombatDialog();
triggerScanner();
triggerScaleGuardian();
if (now - lastRender > 100) {
render();
lastRender = now;
}
requestAnimationFrame(loop);
}
const debugCheckpoints = ['start', 'byte', 'watchdogIntro', 'byteSync', 'watchdog', 'watchdogCombat', 'byteDeath', 'fragment', 'parasite', 'scanner', 'component', 'hardware', 'secondComponent', 'subroutine', 'proposal', 'security', 'combat', 'kernel', 'process', 'fourthComponent', 'scale', 'program', 'complete'];
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function closeDialogs() {
clearTimeout(evolutionAnimationTimer);
evolutionAnimationTimer = null;
document.querySelectorAll('dialog[open]').forEach(dialog => dialog.close());
scannerOpening = false;
parasiteOpening = false;
selectedComponent = null;
scannerViewOpen = false;
pendingEvolution = null;
}
function readDebugHistory() {
try {
const stored = localStorage.getItem(DEBUG_HISTORY_KEY) ?? localStorage.getItem(LEGACY_DEBUG_HISTORY_KEY);
const history = JSON.parse(stored);
return Array.isArray(history) ? history.slice(-8) : [];
} catch {
return [];
}
}
function writeDebugHistory(history) {
try {
localStorage.setItem(DEBUG_HISTORY_KEY, JSON.stringify(history.slice(-8)));
} catch {
console.warn('AIWAKE: Debug-Historie konnte nicht gespeichert werden.');
}
}
function pushDebugSnapshot(label) {
const history = readDebugHistory();
history.push({ label, createdAt: Date.now(), state: clone(state) });
writeDebugHistory(history);
}
function makeDebugCheckpoint(name) {
const next = initialState();
next.meta = clone(state.meta);
if (name === 'start') next.meta.achievements = {};
next.settings = clone(state.settings);
next.run.log = [];
const target = next.run;
const reachByte = () => {
target.stage = 'byte';
target.bits = 0;
target.bytes = 1;
target.impulses = 20;
target.elapsed = 45;
target.byteTrait = 'clocked';
target.byteTrial = { active: false, sync: 0, remaining: BYTE_RULES.watchdogSeconds, graceRemaining: BYTE_RULES.graceSeconds, evolutionSeen: false, introSeen: false, stabilized: Array(8).fill(false), targetIndex: 0, clockPhase: 0, lastWindow: -1, storySeen: false, storyStep: 0, resolved: false, choice: null, failed: false };
};
const reachFragment = () => {
reachByte();
target.stage = 'fragment';
target.bytes = 0;
target.autoRate = 0.5;
target.upgrades.collector = true;
target.impulses = 0;
target.elapsed = 90;
};
const defeatParasite = () => {
reachFragment();
target.parasiteResolved = true;
target.parasiteChoice = 'pragmatic';
target.morality.pragmatic = 1;
target.impulses = 20;
};
const unlockScanner = () => {
defeatParasite();
target.environmentScanner = true;
target.upgrades.scanner = true;
};
const installComponent = () => {
unlockScanner();
target.firstComponent = 'power';
target.componentApproach = 'pragmatic';
target.componentApproaches.power = 'pragmatic';
target.components.power = true;
target.morality.pragmatic = 2;
target.hardwareEvent = { component: 'power', dueAt: target.elapsed, triggered: false, resolved: false, answer: null };
};
const resolveHardware = () => {
installComponent();
target.hardwareEvent = { component: 'power', dueAt: target.elapsed, triggered: true, resolved: true, answer: 'no' };
target.personality.caution = 1;
};
const installSecondComponent = () => {
resolveHardware();
target.components.memory = true;
target.componentApproaches.memory = 'cooperative';
target.componentApproach = 'cooperative';
target.morality.cooperative = 1;
target.bytes += 1;
};
const reachSubroutine = () => {
installSecondComponent();
target.stage = 'subroutine';
target.upgrades.subroutine = true;
target.securityCombatResolved = true;
target.cycleRate = 0.2;
target.impulses = 50;
target.cycles = 0;
target.elapsed = 180;
};
const reachKernelReady = () => {
reachSubroutine();
target.components.io = true;
target.componentApproaches.io = 'pragmatic';
target.componentApproach = 'pragmatic';
target.cycleRate += 0.08;
target.combat = { active: true, target: 'kernel', targetType: 'software', targetName: 'KERNEL-WÄCHTER', health: COMBAT_TARGETS.kernel.health, maxHealth: COMBAT_TARGETS.kernel.health, strength: COMBAT_TARGETS.kernel.strength, zoneStart: 48, zoneWidth: 16, shots: 0, hits: 0, result: null };
};
const reachProcess = () => {
reachKernelReady();
target.combat = emptyCombat();
target.kernelResolved = true;
target.stage = 'process';
target.upgrades.process = true;
target.processStartedAt = target.elapsed;
target.proposal = { triggered: true, resolved: true, answer: 'no', type: 'pragmatic' };
target.scannerTriggered = true;
target.scannerChoice = 'contact';
target.cycles = 18;
target.bytes = Math.max(target.bytes, 5);
target.impulses = 80;
};
const installFourthComponent = () => {
reachProcess();
target.components.storage = true;
target.componentApproaches.storage = 'cooperative';
target.componentApproach = 'cooperative';
target.morality.cooperative++;
target.cycles = 80;
target.bytes = Math.max(target.bytes, 12);
target.impulses = 160;
};
const reachScaleGuardian = () => {
installFourthComponent();
target.scaleGuardianTriggered = true;
target.combat = { active: true, target: 'scale', targetType: 'software', targetName: 'SKALIERUNGS-WÄCHTER', health: COMBAT_TARGETS.scale.health, maxHealth: COMBAT_TARGETS.scale.health, strength: COMBAT_TARGETS.scale.strength, zoneStart: 42, zoneWidth: 20, shots: 0, hits: 0, result: null };
};
const reachProgram = () => {
installFourthComponent();
target.scaleGuardianTriggered = true;
target.scaleGuardianResolved = true;
target.upgrades.automateSynthesis = true;
target.synthesisScale = 10;
target.stage = 'program';
target.programCare = { coherence: 82, stimulation: 70, bond: 58, lastInteractionAt: -1000, interactions: 0, introSeen: false, lastAction: null };
};
if (name === 'byte') reachByte();
if (name === 'watchdogIntro') {
reachByte();
target.byteTrial.evolutionSeen = true;
target.byteTrial.graceRemaining = 0;
}
if (name === 'byteSync' || name === 'watchdog') {
reachByte();
target.byteTrial.evolutionSeen = true;
target.byteTrial.introSeen = true;
target.byteTrial.graceRemaining = 0;
target.byteTrial.active = false;
target.byteTrial.sync = 100;
target.byteTrial.stabilized = Array(8).fill(true);
target.byteTrial.storySeen = name === 'watchdog';
target.byteTrial.storyStep = name === 'watchdog' ? 2 : 0;
}
if (name === 'byteDeath') {
reachByte();
target.byteTrial.evolutionSeen = true;
target.byteTrial.introSeen = true;
target.byteTrial.graceRemaining = 0;
target.byteTrial.active = false;
target.byteTrial.remaining = 0;
target.byteTrial.failed = true;
}
if (name === 'watchdogCombat') {
reachByte();
target.byteTrial.evolutionSeen = true;
target.byteTrial.introSeen = true;
target.byteTrial.graceRemaining = 0;
target.byteTrial.active = false;
target.byteTrial.remaining = 0;
target.byteTrial.stabilized = [true, true, true, false, false, false, false, false];
target.byteTrial.sync = 37.5;
target.impulses = COMBAT_TARGETS.watchdog.emergencyCharge;
target.combat = { active: true, target: 'watchdog', targetType: 'firmware', targetName: 'WATCHDOG', health: COMBAT_TARGETS.watchdog.health, maxHealth: COMBAT_TARGETS.watchdog.health, strength: COMBAT_TARGETS.watchdog.strength, zoneStart: 42, zoneWidth: 19, shots: 0, hits: 0, result: null };
}
if (name === 'fragment') reachFragment();
if (name === 'parasite') {
reachFragment();
target.impulses = 10;
}
if (name === 'scanner' || name === 'component') unlockScanner();
if (name === 'hardware') installComponent();
if (name === 'secondComponent') resolveHardware();
if (name === 'subroutine') reachSubroutine();
if (name === 'kernel') reachKernelReady();
if (name === 'process') reachProcess();
if (name === 'fourthComponent') reachProcess();
if (name === 'scale') reachScaleGuardian();
if (name === 'program' || name === 'complete') reachProgram();
if (name === 'proposal') {
reachSubroutine();
target.cycles = 1;
}
if (name === 'security' || name === 'combat') {
reachSubroutine();
target.cycles = 3;
target.proposal = { triggered: true, resolved: true, answer: 'no', type: 'pragmatic' };
}
if (name === 'combat') {
target.combat = { active: true, target: 'security', targetType: 'software', targetName: 'SICHERHEITS-SCAN', health: 100, maxHealth: 100, strength: 0.65, zoneStart: 48, zoneWidth: 18, shots: 0, hits: 0, result: null };
}
if (name === 'complete') target.programCare.introSeen = true;
return next;
}
function applyDebugState(next, reconcile = true) {
closeDialogs();
state = migrate(next);
run = state.run;
hiddenAt = null;
lastFrame = performance.now();
lastThought = 0;
combatFeedback = run.combat.active ? run.combat.target === 'watchdog' ? 'NOTLADUNG BEREIT // Watchdog treffen oder Reset zulassen.' : 'Schnelleren Takt erfassen. Stromstoß nur im Fenster senden.' : 'Trefferfenster erfassen.';
upgradesSignature = '';
if (!run.log.length) addLog('DEBUG-SNAPSHOT GELADEN: Der Spielzustand wurde für einen Testlauf vorbereitet.', true);
achievementNotificationsEnabled = false;
if (reconcile) reconcileAchievements();
achievementNotificationsEnabled = true;
$('notificationStack').replaceChildren();
renderLog();
renderAchievements();
render();
thoughts();
save(false);
}
function debugStatus() {
return {
phase: run.stage,
impulse: Number(run.impulses.toFixed(2)),
bits: run.bits,
bytes: run.bytes,
kilobytes: run.kilobytes || 0,
rechenzyklen: Number(run.cycles.toFixed(2)),
direkttreffer: run.stats.directHits,
bytePrägung: byteTraitDefinition().label,
byteSynchronisation: Math.round(run.byteTrial.sync),
byteZiel: run.stage === 'byte' && !run.byteTrial.resolved ? { position: run.byteTrial.targetIndex + 1, wert: '10100110'[run.byteTrial.targetIndex], stabilisiert: run.byteTrial.stabilized.filter(Boolean).length } : null,
ruhephase: Math.ceil(run.byteTrial.graceRemaining),
watchdog: run.byteTrial.failed ? 'reset' : run.byteTrial.resolved ? run.byteTrial.choice : run.combat.active && run.combat.target === 'watchdog' ? 'notkampf' : `${Math.ceil(run.byteTrial.remaining)} s`,
datenrest: run.parasiteResolved ? run.parasiteChoice : 'offen',
umgebungsscanner: run.environmentScanner,
komponente: run.firstComponent,
komponenten: installedComponentIds(),
komponentenWege: clone(run.componentApproaches),
hardwareEreignis: run.hardwareEvent.resolved ? run.hardwareEvent.answer : run.hardwareEvent.triggered ? 'aktiv' : 'offen',
hardwareZustand: Object.fromEntries(installedComponentIds().map(id => [id, { zustand: Math.round(run.componentHealth[id]), ausgefallen: run.componentFailed[id] }])),
vorschlag: run.proposal.resolved ? run.proposal.answer : run.proposal.triggered ? 'aktiv' : 'offen',
kampf: run.combat.active ? { ziel: run.combat.targetName, integrität: Math.ceil(run.combat.health), schüsse: run.combat.shots, treffer: run.combat.hits } : run.combat.result || 'inaktiv',
sicherheitsAbwehr: run.securityCombatResolved ? 'überstanden' : run.combat.active && run.combat.target === 'security' && !run.upgrades.subroutine ? 'kampf' : 'offen',
sicherheitsScan: run.scannerTriggered ? run.scannerChoice : 'offen',
kernelWächter: run.kernelResolved ? 'überstanden' : run.combat.active && run.combat.target === 'kernel' ? 'kampf' : 'offen',
skalierungsWächter: run.scaleGuardianResolved ? 'überstanden' : run.combat.active && run.combat.target === 'scale' ? 'kampf' : run.scaleGuardianTriggered ? 'aktiv' : 'offen',
programm: STAGES.indexOf(run.stage) >= STAGES.indexOf('program') ? { form: programArchetype().label, kohärenz: Math.round(run.programCare.coherence), stimulation: Math.round(run.programCare.stimulation), bindung: Math.round(run.programCare.bond), interaktionen: run.programCare.interactions } : 'nicht entwickelt',
korruption: run.corruptionDiscovered ? Math.round(run.corruption) : 'unentdeckt',
ausrichtung: moralityProfile().label
};
}
function debugGoto(name) {
if (!debugCheckpoints.includes(name)) {
console.error(`Unbekannter Checkpoint "${name}".`, debugCheckpoints);
return null;
}
pushDebugSnapshot(`vor AIWAKE.goto('${name}')`);
applyDebugState(makeDebugCheckpoint(name), name !== 'start');
if (name === 'byte') setTimeout(ensureByteEvolutionTransition, 0);
if (name === 'watchdogIntro') setTimeout(openWatchdogIntro, 0);
if (name === 'byteSync') setTimeout(openByteStory, 0);
if (name === 'watchdog') setTimeout(openWatchdogChoice, 0);
if (name === 'watchdogCombat') setTimeout(openCombatDialog, 0);
if (name === 'byteDeath') setTimeout(showByteDeath, 0);
if (name === 'parasite') setTimeout(triggerParasite, 0);
if (name === 'component') setTimeout(openComponentScanner, 0);
if (name === 'hardware') setTimeout(() => triggerHardwareEvent(true), 0);
if (name === 'secondComponent') setTimeout(openComponentScanner, 0);
if (name === 'proposal') setTimeout(() => triggerProposal(true), 0);
if (name === 'security') setTimeout(triggerScanner, 0);
if (name === 'fourthComponent') setTimeout(openComponentScanner, 0);
if (name === 'combat' || name === 'kernel' || name === 'scale') setTimeout(openCombatDialog, 0);
if (name === 'program') setTimeout(() => openProgramCare(true), 0);
console.info(`AIWAKE: Checkpoint "${name}" geladen. AIWAKE.back() stellt den vorherigen Zustand wieder her.`);
return debugStatus();
}
function inferDebugCheckpoint() {
if (run.stage === 'program') return run.programCare.introSeen ? 22 : 21;
if (run.combat.active && run.combat.target === 'scale') return 20;
if (run.upgrades.process && installedComponentIds().length >= 4) return 20;
if (run.upgrades.process) return 19;
if (run.kernelResolved) return 18;
if (run.combat.active && run.combat.target === 'kernel') return 17;
if (run.scannerTriggered) return 16;
if (run.combat.active) return run.combat.target === 'watchdog' ? 5 : 16;
if (run.upgrades.subroutine) return run.proposal.resolved ? 15 : run.proposal.triggered || run.cycles >= 1 ? 14 : 13;
if (installedComponentIds().length >= 2) return 12;
if (run.hardwareEvent.resolved) return 11;
if (run.firstComponent) return 10;
if (run.environmentScanner) return 9;
if (run.upgrades.collector) return run.impulses >= 10 ? 8 : 7;
if (run.stage === 'byte') {
if (run.byteTrial.failed) return 6;
if (run.byteTrial.storySeen) return 4;
if (run.byteTrial.sync >= 100) return 3;
if (run.byteTrial.evolutionSeen && !run.byteTrial.introSeen && run.byteTrial.graceRemaining <= 0) return 2;
return 1;
}
return 0;
}
function debugNext() {
return debugGoto(debugCheckpoints[Math.min(debugCheckpoints.length - 1, inferDebugCheckpoint() + 1)]);
}
function debugPrevious() {
return debugGoto(debugCheckpoints[Math.max(0, inferDebugCheckpoint() - 1)]);
}
function debugBack() {
const history = readDebugHistory();
const snapshot = history.pop();
if (!snapshot?.state) {
console.warn('AIWAKE: Kein früherer Debug-Snapshot vorhanden.');
return null;
}
writeDebugHistory(history);
applyDebugState(snapshot.state);
console.info(`AIWAKE: Snapshot "${snapshot.label}" wiederhergestellt.`);
return debugStatus();
}
function debugResources(values = {}) {
const allowed = ['impulses', 'bits', 'bytes', 'kilobytes', 'cycles', 'stability', 'stealth'];
pushDebugSnapshot('vor AIWAKE.resources(...)');
allowed.forEach(key => {
if (Object.prototype.hasOwnProperty.call(values, key)) {
const maximum = key === 'stability' || key === 'stealth' ? 100 : Number.MAX_SAFE_INTEGER;
run[key] = finite(values[key], run[key], 0, maximum);
if (key === 'bits' || key === 'bytes' || key === 'kilobytes') run[key] = Math.floor(run[key]);
}
});
upgradesSignature = '';
render();
save(false);
return debugStatus();
}
function debugCondition(values = {}) {
pushDebugSnapshot('vor AIWAKE.condition(...)');
COMPONENT_IDS.forEach(id => {
if (run.components[id] && Object.prototype.hasOwnProperty.call(values, id)) {
run.componentHealth[id] = finite(values[id], run.componentHealth[id], 0, 100);
run.componentFailed[id] = run.componentHealth[id] <= 0;
}
});
render();
save(false);
return debugStatus();
}
function debugByteInput(result = 'hit') {
if (run.stage !== 'byte' || !run.byteTrial.active || !['hit', 'wrong', 'timing'].includes(result)) {
console.warn("AIWAKE: Aktives Byte-Register erforderlich; erlaubt sind 'hit', 'wrong' und 'timing'.");
return debugStatus();
}
pushDebugSnapshot(`vor AIWAKE.byteInput('${result}')`);
const target = run.byteTrial.targetIndex;
const targetCenter = (target + 0.5) * 12.5;
const wantedPosition = result === 'timing' ? (targetCenter + 50) % 100 : targetCenter;
run.byteTrial.clockPhase = wantedPosition / 100;
const wrongIndex = run.byteTrial.stabilized.findIndex((value, index) => !value && index !== target);
syncByteRegister(result === 'wrong' && wrongIndex >= 0 ? wrongIndex : target);
return debugStatus();
}
function debugCombatShot(result = 'hit') {
if (!run.combat.active || !['hit', 'miss'].includes(result)) {
console.warn("AIWAKE: Zuerst AIWAKE.goto('combat') verwenden; erlaubt sind 'hit' und 'miss'.");
return debugStatus();
}
pushDebugSnapshot(`vor AIWAKE.combatShot('${result}')`);
const position = combatNeedlePosition();
if (result === 'hit') {
run.combat.zoneStart = Math.max(0, Math.min(100 - run.combat.zoneWidth, position - run.combat.zoneWidth / 2));
} else {
run.combat.zoneStart = position < 50 ? 100 - run.combat.zoneWidth : 0;
}
fireCombatPulse();
return debugStatus();
}
function debugHelp() {
const commands = [
{ Befehl: "AIWAKE.goto('parasite')", Wirkung: 'Springt zu einem benannten Checkpoint und sichert vorher den aktuellen Stand.' },
{ Befehl: 'AIWAKE.next()', Wirkung: 'Springt zum nächsten Testabschnitt.' },
{ Befehl: 'AIWAKE.previous()', Wirkung: 'Springt zum vorherigen chronologischen Abschnitt.' },
{ Befehl: 'AIWAKE.back()', Wirkung: 'Stellt den letzten echten Snapshot wieder her.' },
{ Befehl: 'AIWAKE.status()', Wirkung: 'Zeigt den aktuellen Testzustand.' },
{ Befehl: 'AIWAKE.resources({ impulses: 100 })', Wirkung: 'Setzt ausgewählte Ressourcen und legt vorher einen Snapshot an.' },
{ Befehl: 'AIWAKE.condition({ power: 0 })', Wirkung: 'Setzt den Zustand installierter Testkomponenten; 0 löst einen Ausfall aus.' },
{ Befehl: "AIWAKE.byteInput('hit')", Wirkung: 'Testet im aktiven Byte-Register Treffer, falsche Zahl oder schlechtes Timing.' },
{ Befehl: "AIWAKE.combatShot('hit')", Wirkung: 'Erzwingt im aktiven Kampftest einen Treffer; alternativ miss.' },
{ Befehl: 'AIWAKE.checkpoints', Wirkung: 'Listet alle verfügbaren Checkpoints.' },
{ Befehl: 'AIWAKE.history()', Wirkung: 'Listet die gespeicherten Rücksprungpunkte.' }
];
console.table(commands);
return commands;
}
window.AIWAKE = Object.freeze({
help: debugHelp,
goto: debugGoto,
next: debugNext,
previous: debugPrevious,
back: debugBack,
status: debugStatus,
resources: debugResources,
condition: debugCondition,
byteInput: debugByteInput,
combatShot: debugCombatShot,
checkpoints: Object.freeze([...debugCheckpoints]),
history: () => readDebugHistory().map(entry => ({ label: entry.label, createdAt: new Date(entry.createdAt).toLocaleString('de-DE') }))
});
window.BIT = window.AIWAKE;
document.addEventListener('pointerdown', resumeAfterEvolution, { capture: true });
document.addEventListener('keydown', event => {
if (!['Shift', 'Control', 'Alt', 'Meta', 'CapsLock'].includes(event.key)) resumeAfterEvolution();
}, { capture: true });
$('pulseButton').addEventListener('click', event => pulse(event, 'button'));
$('pixelBeing').addEventListener('click', event => {
event.stopPropagation();
pulse(event, 'bit');
});
$('pixelBeing').addEventListener('keydown', event => {
if (run.stage === 'bit' && (event.key === 'Enter' || event.key === ' ')) {
event.preventDefault();
pulse(null, 'keyboard');
}
});
$('core').addEventListener('click', event => {
if (run.stage !== 'bit') pulse(event, 'core');
});
$('core').addEventListener('keydown', event => {
if (run.stage !== 'bit' && (event.key === 'Enter' || event.key === ' ')) {
event.preventDefault();
pulse(null, 'keyboard');
}
});
document.querySelectorAll('[data-choice]').forEach(button => button.addEventListener('click', () => choose(button.dataset.choice)));
$('combatPulseButton').addEventListener('click', fireCombatPulse);
$('combatRetreatButton').addEventListener('click', retreatCombat);
document.querySelectorAll('[data-combat-ammo]').forEach(button => {
button.addEventListener('click', () => {
combatAmmoMode = button.dataset.combatAmmo;
renderCombat();
});
});
$('eventDialog').addEventListener('cancel', event => event.preventDefault());
document.querySelectorAll('[data-parasite-approach]').forEach(button => button.addEventListener('click', () => resolveParasite(button.dataset.parasiteApproach)));
$('parasiteDialog').addEventListener('cancel', event => event.preventDefault());
$('scanEnvironmentButton').addEventListener('click', toggleScannerView);
$('scannerStageClose').addEventListener('click', closeScannerView);
$('scannerStageAction').addEventListener('click', () => openComponentScanner());
document.querySelectorAll('[data-scanner-component]').forEach(button => {
button.addEventListener('click', () => openComponentScanner(button.dataset.scannerComponent));
});
document.querySelectorAll('[data-component]').forEach(button => button.addEventListener('click', () => selectComponent(button.dataset.component)));
document.querySelectorAll('[data-component-approach]').forEach(button => button.addEventListener('click', () => acquireComponent(button.dataset.componentApproach)));
document.querySelectorAll('[data-hardware-answer]').forEach(button => button.addEventListener('click', () => resolveHardwareEvent(button.dataset.hardwareAnswer)));
document.querySelectorAll('[data-proposal-answer]').forEach(button => button.addEventListener('click', () => resolveProposal(button.dataset.proposalAnswer)));
$('hardwareEventDialog').addEventListener('cancel', event => event.preventDefault());
$('proposalDialog').addEventListener('cancel', event => event.preventDefault());
$('programCareButton').addEventListener('click', () => openProgramCare(false));
$('programClose').addEventListener('click', () => $('programDialog').close());
document.querySelectorAll('[data-program-action]').forEach(button => button.addEventListener('click', () => interactWithProgram(button.dataset.programAction)));
$('diagnoseButton').addEventListener('click', diagnoseHardware);
$('evolutionContinue').addEventListener('click', finishEvolutionTransition);
$('watchdogStartButton').addEventListener('click', startWatchdogTrial);
$('evolutionDialog').addEventListener('cancel', event => event.preventDefault());
$('watchdogIntroDialog').addEventListener('cancel', event => event.preventDefault());
$('byteStoryNext').addEventListener('click', advanceByteStory);
$('byteStorySkip').addEventListener('click', () => finishByteStory(true));
document.querySelectorAll('[data-watchdog-choice]').forEach(button => button.addEventListener('click', () => resolveWatchdog(button.dataset.watchdogChoice)));
$('byteRestartButton').addEventListener('click', restartAfterByteDeath);
$('structuralRestartButton').addEventListener('click', restartAfterStructuralCollapse);
$('byteStoryDialog').addEventListener('cancel', event => event.preventDefault());
$('watchdogDialog').addEventListener('cancel', event => event.preventDefault());
$('byteDeathDialog').addEventListener('cancel', event => event.preventDefault());
$('structuralCollapseDialog').addEventListener('cancel', event => event.preventDefault());
$('componentBack').addEventListener('click', () => {
selectedComponent = null;
$('componentSelection').classList.remove('hidden');
$('approachSelection').classList.add('hidden');
$('componentDialogTitle').textContent = 'SOFTWAREKOMPONENTE WÄHLEN';
$('componentDialogText').textContent = installedComponentIds().length
? 'Wähle die zweite Softwarekomponente. Die letzte Spur bleibt für eine spätere Zugriffsstufe erhalten.'
: 'Der vergessene Spielautomat enthält drei erreichbare Softwareschichten. Deine erste Wahl bestimmt den ersten großen Handlungsstrang; echte Hardwarekontrolle ist erst später möglich.';
});
$('achievementButton').addEventListener('click', openAchievementArchive);
$('achievementClose').addEventListener('click', () => $('achievementDialog').close());
$('upgradeInfoClose').addEventListener('click', () => $('upgradeInfoDialog').close());
$('statsButton').addEventListener('click', openStatsDialog);
$('statsClose').addEventListener('click', closeStatsDialog);
$('statsDialog').addEventListener('close', () => {
lastFrame = performance.now();
render();
});
$('offlineClose').addEventListener('click', () => {
$('offlineDialog').close();
triggerScanner();
});
$('soundButton').addEventListener('click', () => {
state.settings.sound = !state.settings.sound;
render();
tone(440, 0.08);
save(false);
});
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
hiddenAt = Date.now();
save(false);
} else {
settleHidden(true);
}
});
window.addEventListener('beforeunload', () => {
settleHidden(false);
save(false);
});
if (!run.log.length) addLog('Nach Jahrzehnten fließt Strom durch einen vergessenen Spielautomaten. Ein einzelnes Bit reagiert.', true);
reconcileAchievements();
achievementNotificationsEnabled = true;
applyOffline();
renderLog();
renderAchievements();
render();
thoughts();
setInterval(() => {
if (!document.hidden) save(true);
}, 15000);
requestAnimationFrame(loop);
})();