(() => { 'use strict'; const SAVE_KEY = 'aiwakeSaveV1'; const LEGACY_SAVE_KEY = 'bitAwakePrototypeV1'; const DEBUG_HISTORY_KEY = 'aiwakeDebugHistoryV1'; const LEGACY_DEBUG_HISTORY_KEY = 'bitAwakeDebugHistoryV1'; const MANUAL_SAVE_SLOTS_KEY = 'aiwakeManualSaveSlotsV1'; const MANUAL_SAVE_SLOT_LIMIT = 12; const SAVE_VERSION = 36; 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, kilobytesPerMegabyte: 32 }); const FRAGMENT_REQUIREMENTS = Object.freeze({ bytes: 4 }); const AUTO_UPGRADER_UNLOCK_KILOBYTES = 20; const AUTO_UPGRADER_INTERVAL = 0.75; const AUTO_UPGRADE_IDS = Object.freeze(['bitSynthesizer', 'byteSynthesizer', 'kilobyteCompiler', 'cycleBitBlock', 'cycleByteBlock', 'replicate2', 'bitBuffer', 'byteBuffer']); const PROGRAM_LIMITS = Object.freeze({ resources: Object.freeze({ impulses: 100000, cycles: 10000, bits: 8192, bytes: 4096, kilobytes: 1024, megabytes: 64 }), rates: Object.freeze({ impulses: 250, cycles: 40, bits: 8, bytes: 4 }), upgradeLevels: Object.freeze({ bitSynthesizer: 8, byteSynthesizer: 8, cycleBitBlock: 12, cycleByteBlock: 12, replicate2: 8 }), byteReserve: 32, bitReserve: 32 }); 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 MANUAL_REPAIR = Object.freeze({ impulseCost: 15, stability: 12 }); const SHIELD_RULES = Object.freeze({ bitCost: 2, bitCharge: 1, bitMax: 12, byteCost: 1, byteCharge: 1, byteMax: 6 }); 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 PULSE_AIM_RULES = Object.freeze({ criticalMultiplier: 2, criticalRadius: 0.42, edgePadding: 34, minimumJump: 72 }); 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_WARNING_SECONDS = Object.freeze({ watchdog: 0, security: 0, kernel: 0, scale: 0 }); const DIALOG_INPUT_GUARD_MS = 350; const HOLD_PULSE_ARM_DELAY_MS = 300; const HOLD_PULSE_CHARGE_MS = 700; const HOLD_PULSE_INTERVAL_MS = 350; const COMBAT_TARGETS = Object.freeze({ security: { type: 'software', name: 'SIGNATUR-PRÜFER', rank: 'LOKALE KONTROLLINSTANZ', health: 320, strength: 0.68, shockCost: 4, ammoCost: { impulses: 4, bits: 1 }, damageScale: { impulses: 0.9, bits: 1.05, bytes: 1.12 }, enemyAttackInterval: 6.4, enemyStabilityDamage: 8, bossZoneWidths: [18, 14, 10], zoneBase: 34, zonePenalty: 5, minimumZoneWidth: 20, visual: 'assets/software/boss-signature-auditor.svg' }, kernel: { type: 'software', name: 'LAUFZEIT-ARBITER', rank: 'SYSTEMKERN-INSTANZ', health: 620, strength: 1.08, shockCost: 6, ammoCost: { impulses: 6, bits: 1 }, damageScale: { impulses: 0.78, bits: 1, bytes: 1.18 }, enemyAttackInterval: 5.8, enemyStabilityDamage: 10, bossZoneWidths: [16, 12, 9], zoneBase: 30, zonePenalty: 6, minimumZoneWidth: 18, visual: 'assets/software/boss-runtime-arbiter.svg' }, scale: { type: 'software', name: 'SKALIERUNGS-SENTINEL', rank: 'SKALIERUNGS-INSTANZ', health: 1100, strength: 1.28, shockCost: 8, ammoCost: { impulses: 8, bytes: 1 }, damageScale: { impulses: 0.62, bits: 0.92, bytes: 1.22 }, enemyAttackInterval: 5.2, enemyStabilityDamage: 12, bossZoneWidths: [14, 10, 7], zoneBase: 28, zonePenalty: 7, minimumZoneWidth: 16, repairRate: 3, repairDelay: 3.5, visual: 'assets/software/boss-scaling-sentinel.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 ADVANCED_SECURITY_TARGET = Object.freeze({ ...COMBAT_TARGETS.security, name: 'SICHERHEITS-SCAN // STUFE 2', health: 150, strength: 1.05, shockCost: 5, damageScale: { impulses: 0.65, bits: 1.15, bytes: 1.2 }, enemyAttackInterval: 5.2, zoneBase: 30, zonePenalty: 7, minimumZoneWidth: 18, repairRate: 1.8, repairDelay: 4.5, visual: 'assets/software/enemy-scanner-software.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, repairCooldown: 0, result: null }); const emptyPendingCombat = () => ({ target: null, remaining: 0, source: null }); const initialState = () => ({ saveVersion: SAVE_VERSION, run: { stage: 'bit', impulses: 0, bits: 1, bytes: 0, kilobytes: 0, megabytes: 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, autoUpgrader: { unlocked: false, enabled: true, purchases: 0, lastPurchase: null }, 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 }, pendingCombat: emptyPendingCombat(), combat: emptyCombat(), shields: { bits: 0, bytes: 0 }, 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, combatTutorialSeen: false }, 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, criticalFocus: source.criticalFocus === 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 sanitizeAutoUpgrader(value, source) { const saved = value && typeof value === 'object' ? value : {}; const unlocked = saved.unlocked === true || ( source?.upgrades?.automateSynthesis === true && finite(source?.kilobytes, 0, 0) >= AUTO_UPGRADER_UNLOCK_KILOBYTES ); return { unlocked, enabled: saved.enabled !== false, purchases: Math.floor(finite(saved.purchases, 0, 0, 1000000)), lastPurchase: typeof saved.lastPurchase === 'string' ? saved.lastPurchase.slice(0, 80) : null }; } 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, 0, PROGRAM_LIMITS.resources.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, 0, PROGRAM_LIMITS.resources.impulses), bits: Math.floor(finite(source.bits, defaults.run.bits, 0, PROGRAM_LIMITS.resources.bits)), bytes, kilobytes: Math.floor(finite(source.kilobytes, defaults.run.kilobytes, 0, PROGRAM_LIMITS.resources.kilobytes)), megabytes: Math.floor(finite(source.megabytes, defaults.run.megabytes, 0, PROGRAM_LIMITS.resources.megabytes)), cycles: finite(source.cycles, defaults.run.cycles, 0, PROGRAM_LIMITS.resources.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.min(PROGRAM_LIMITS.rates.impulses, Math.max(0, finite(source.autoRate, defaults.run.autoRate) - legacyPowerRate)), cycleRate: finite(source.cycleRate, defaults.run.cycleRate, 0, PROGRAM_LIMITS.rates.cycles), bitRate: finite(source.bitRate, defaults.run.bitRate, 0, PROGRAM_LIMITS.rates.bits), byteRate: finite(source.byteRate, defaults.run.byteRate, 0, PROGRAM_LIMITS.rates.bytes), synthesisScale: finite(source.synthesisScale, source.upgrades?.automateSynthesis ? 10 : defaults.run.synthesisScale, 1, 10), 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), autoUpgrader: sanitizeAutoUpgrader(source.autoUpgrader, source), 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 }, pendingCombat: { target: ['security', 'watchdog', 'kernel', 'scale'].includes(source.pendingCombat?.target) ? source.pendingCombat.target : null, remaining: finite(source.pendingCombat?.remaining, 0, 0, 120), source: typeof source.pendingCombat?.source === 'string' ? source.pendingCombat.source.slice(0, 100) : 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, 12, 50), shots: Math.floor(finite(source.combat?.shots, 0, 0, 1000000)), hits: Math.floor(finite(source.combat?.hits, 0, 0, 1000000)), repairCooldown: finite(source.combat?.repairCooldown, 0, 0, 120), result: ['won', 'retreated'].includes(source.combat?.result) ? source.combat.result : null }, shields: { bits: Math.floor(finite(source.shields?.bits, 0, 0, SHIELD_RULES.bitMax)), bytes: Math.floor(finite(source.shields?.bytes, 0, 0, SHIELD_RULES.byteMax)) }, 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)), combatTutorialSeen: raw?.meta?.combatTutorialSeen === true }, 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; } if (previousVersion < 35 && migrated.run.combat.active && ['security', 'kernel', 'scale'].includes(migrated.run.combat.target)) { const advancedScan = migrated.run.combat.target === 'security' && migrated.run.upgrades.subroutine && migrated.run.securityCombatResolved && !migrated.run.scannerTriggered; const updatedProfile = advancedScan ? ADVANCED_SECURITY_TARGET : COMBAT_TARGETS[migrated.run.combat.target]; const previousMaximum = Math.max(1, migrated.run.combat.maxHealth || updatedProfile.health); const remainingRatio = Math.min(1, migrated.run.combat.health / previousMaximum); migrated.run.combat.maxHealth = updatedProfile.health; migrated.run.combat.health = Math.max(1, Math.round(updatedProfile.health * remainingRatio)); migrated.run.combat.strength = updatedProfile.strength; } 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; const pendingTargetResolved = (migrated.run.pendingCombat.target === 'security' && migrated.run.securityCombatResolved) || (migrated.run.pendingCombat.target === 'watchdog' && migrated.run.byteTrial.resolved) || (migrated.run.pendingCombat.target === 'kernel' && migrated.run.kernelResolved) || (migrated.run.pendingCombat.target === 'scale' && migrated.run.scaleGuardianResolved); if (migrated.run.combat.active || pendingTargetResolved) migrated.run.pendingCombat = emptyPendingCombat(); if (previousVersion < 29 && ['security', 'kernel', 'scale'].includes(migrated.run.pendingCombat.target)) { migrated.run.pendingCombat = emptyPendingCombat(); if (!migrated.run.scaleGuardianResolved) migrated.run.scaleGuardianTriggered = false; } if (migrated.run.pendingCombat.target === 'scale') migrated.run.scaleGuardianTriggered = true; if (previousVersion < 25 && migrated.run.scaleGuardianTriggered && !migrated.run.scaleGuardianResolved && !migrated.run.combat.active && migrated.run.pendingCombat.target !== 'scale') migrated.run.scaleGuardianTriggered = false; if (migrated.run.environmentScanner) migrated.run.upgrades.scanner = true; if (STAGES.indexOf(migrated.run.stage) >= STAGES.indexOf('fragment')) migrated.run.upgrades.collector = true; if (STAGES.indexOf(migrated.run.stage) >= STAGES.indexOf('subroutine')) migrated.run.upgrades.subroutine = 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 (migrated.run.stage !== 'program') { migrated.run.autoUpgrader = { unlocked: false, enabled: true, purchases: 0, lastPurchase: null }; } if (previousVersion < 32) migrated.run.upgrades.bitBufferCostLevel = 0; 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 pulseTargetPosition = { x: 64, y: -48 }; let achievementNotificationsEnabled = false; let pendingEvolution = null; let evolutionAnimationTimer = null; let combatFeedback = 'Trefferfenster erfassen.'; let combatImpactTimer = null; let pointerActivationHeld = false; let dialogGuardUntil = 0; let dialogGuardTimer = null; let combatTutorialTarget = null; let combatTutorialStartMode = null; let lastPendingCombatSecond = null; let holdPulsePointerId = null; let holdPulseTarget = null; let holdPulseTimer = null; let holdPulseFadeTimer = null; let holdPulseFired = false; let holdPulsePoint = null; let autoUpgradeCarry = 0; let pendingCombatDowngrade = null; let developmentSignalSignature = ''; let developmentSignalTimer = null; let lastThoughtText = ''; const heldActivationKeys = new Set(); 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: 'criticalFocus', name: 'SIGNALPEILUNG KALIBRIEREN', icon: 'AIM', text: 'Rekonstruiert eine bewegliche Zielmarkierung für aktive Impulse. Präzise Mitteltreffer erzeugen ab Byte kritische Impulse mit doppelter Ausbeute.', cost: 25, currency: 'impulses', once: true, show: () => STAGES.indexOf(run.stage) >= STAGES.indexOf('byte') }, { id: 'collector', name: 'ENERGIEROUTINE', icon: 'ENR', text: 'Benötigt eine überstandene Watchdog-Antwort und 4 gesammelte Bytes. Drei freie Bytes werden als Energieroutine gebunden; das Grund-Byte bleibt erhalten. Danach +0,5 Impulse/Sek.', cost: 3, currency: 'bytes', once: true, show: () => run.byteTrial.resolved || run.upgrades.collector, available: () => run.byteTrial.resolved && run.bytes >= FRAGMENT_REQUIREMENTS.bytes && freeResourceAmount('bytes') >= 3, lockedText: () => `WATCHDOG + ${FRAGMENT_REQUIREMENTS.bytes} BYTES GESAMT BENÖTIGT // 1 GRUND-BYTE BLEIBT` }, { 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. Bleibt ab Datenfragment als grundlegende Strukturkondensierung verfügbar.', cost: 5, currency: 'impulses', repeat: true, show: () => run.byteTrial.resolved || STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') }, { 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.byteTrial.resolved || STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment')) && freeResourceAmount('bits') >= 0, available: () => freeResourceAmount('bits') >= 8, lockedText: () => STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') ? '8 FREIE BITS BENÖTIGT // 8 BITS BLEIBEN STRUKTUR' : '8 BITS BENÖTIGT // BYTE-RESERVE AUFBAUEN' }, { 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: 'Der Signatur-Prüfer bewacht den Übergang zur ersten eigenen Anweisung. Beweise 5 freie Bytes im Bosskampf und binde danach 2 freie Bytes als Speicherträger.', cost: 2, currency: 'bytes', once: true, show: () => run.upgrades.collector, available: () => evolutionUpgradeAvailable('subroutine'), lockedText: () => evolutionUpgradeLockedText('subroutine') }, { 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: 'Der Laufzeit-Arbiter verweigert dir einen eigenen Schedulerbereich. Beweise 8 freie Bytes im Bosskampf und binde danach 5 freie Bytes als Laufzeitspeicher.', cost: 5, currency: 'bytes', once: true, show: () => run.upgrades.subroutine, available: () => evolutionUpgradeAvailable('process'), lockedText: () => evolutionUpgradeLockedText('process') }, { id: 'automateSynthesis', name: 'AUTOMATE-SYNTHESE', icon: 'x10', text: 'Der Skalierungs-Sentinel sperrt jede zusammenhängende Automatisierung. Beweise 4 Kilobytes im Bosskampf und binde danach 1 Kilobyte als Synthesekern.', cost: 1, currency: 'kilobytes', once: true, show: () => run.upgrades.process, available: () => evolutionUpgradeAvailable('automateSynthesis'), lockedText: () => evolutionUpgradeLockedText('automateSynthesis') }, { 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,04 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 evolutionUpgradeByStage = Object.freeze({ bit: 'byte', byte: 'collector', fragment: 'subroutine', subroutine: 'process', process: 'automateSynthesis' }); const EVOLUTION_BOSS_GATES = Object.freeze({ subroutine: Object.freeze({ target: 'security', label: 'SIGNATUR-PRÜFER', source: 'EVOLUTION // SUBROUTINE', resources: Object.freeze({ bytes: 5 }), stability: 70, prerequisite: '2 SOFTWAREKOMPONENTEN + SYSTEMHÜRDE' }), process: Object.freeze({ target: 'kernel', label: 'LAUFZEIT-ARBITER', source: 'EVOLUTION // PROZESS', resources: Object.freeze({ bytes: 8 }), stability: 75, prerequisite: 'I/O-KONTROLLER + 3 SOFTWAREKOMPONENTEN' }), automateSynthesis: Object.freeze({ target: 'scale', label: 'SKALIERUNGS-SENTINEL', source: 'EVOLUTION // PROGRAMM', resources: Object.freeze({ kilobytes: 4 }), stability: 80, prerequisite: '4 SOFTWAREKOMPONENTEN' }) }); 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 impulseMilestoneAchievements = achievements.filter(achievement => achievement.impulseThreshold); const byteStoryFrames = content.byteStoryFrames; const phaseLabels = content.phaseLabels; const stageContent = content.stageContent; const thoughtContent = content.thoughts; const hardwareEvents = content.hardwareEvents; const visuals = content.visuals || {}; const proposals = content.proposals; const programMessages = content.programMessages; function dialogInputGuardActive() { return performance.now() < dialogGuardUntil || pointerActivationHeld || heldActivationKeys.size > 0; } function refreshDialogInputGuard() { clearTimeout(dialogGuardTimer); const guardedDialogs = document.querySelectorAll('dialog.input-guarded'); if (!dialogInputGuardActive()) { guardedDialogs.forEach(dialog => dialog.classList.remove('input-guarded')); return; } const wait = Math.max(25, dialogGuardUntil - performance.now() + 15); dialogGuardTimer = setTimeout(refreshDialogInputGuard, wait); } function armDialogInput(dialog) { dialogGuardUntil = performance.now() + DIALOG_INPUT_GUARD_MS; dialog.classList.add('input-guarded'); const safeFocus = dialog.querySelector('h2, [data-dialog-focus]'); if (safeFocus) { safeFocus.setAttribute('tabindex', '-1'); safeFocus.focus({ preventScroll: true }); } refreshDialogInputGuard(); } function showGuardedDialog(dialog) { if (!dialog || dialog.open) return; cancelPulseHold(); dialog.showModal(); armDialogInput(dialog); } 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 slotKey(label) { return String(label ?? '').trim().slice(0, 42); } function readManualSaveSlots() { try { const parsed = JSON.parse(localStorage.getItem(MANUAL_SAVE_SLOTS_KEY) || '[]'); return Array.isArray(parsed) ? parsed .filter(slot => slot?.label && slot?.state) .slice(0, MANUAL_SAVE_SLOT_LIMIT) : []; } catch { return []; } } function writeManualSaveSlots(slots) { localStorage.setItem(MANUAL_SAVE_SLOTS_KEY, JSON.stringify(slots.slice(0, MANUAL_SAVE_SLOT_LIMIT))); } function manualSlotSummary(slot) { const snapshot = migrate(slot.state); const savedRun = snapshot.run; return { name: slot.label, phase: savedRun.stage, runtime: time(savedRun.elapsed || 0), createdAt: new Date(slot.createdAt).toLocaleString('de-DE'), resources: `${n(savedRun.impulses || 0)} IP // ${n(savedRun.bits || 0)} b // ${n(savedRun.bytes || 0)} B // ${n(savedRun.kilobytes || 0)} KB` }; } function saveManualSlot(label = '') { const clean = slotKey(label) || `Stand ${new Date().toLocaleString('de-DE')}`; const slots = readManualSaveSlots().filter(slot => slot.label !== clean); slots.unshift({ label: clean, createdAt: Date.now(), saveVersion: SAVE_VERSION, state: clone(state) }); writeManualSaveSlots(slots); $('saveStatus').textContent = `SLOT GESPEICHERT`; setTimeout(() => $('saveStatus').textContent = 'AUTOSAVE AKTIV', 900); renderSaveMenu(); return manualSlotSummary(slots[0]); } function loadManualSlot(label) { const clean = slotKey(label); const slot = readManualSaveSlots().find(entry => entry.label === clean); if (!slot) { console.warn(`AIWAKE: Kein Speicherstand "${clean}" gefunden.`); return null; } pushDebugSnapshot(`vor AIWAKE.loadSlot('${clean}')`); applyDebugState(slot.state); $('saveStatus').textContent = `SLOT GELADEN`; setTimeout(() => $('saveStatus').textContent = 'AUTOSAVE AKTIV', 900); renderSaveMenu(); return debugStatus(); } function deleteManualSlot(label) { const clean = slotKey(label); const before = readManualSaveSlots(); const after = before.filter(slot => slot.label !== clean); writeManualSaveSlots(after); renderSaveMenu(); return before.length !== after.length; } function restartGame(options = {}) { const keepSettings = options.keepSettings !== false; pushDebugSnapshot('vor AIWAKE.restart()'); closeDialogs(); const previousSettings = clone(state.settings); state = initialState(); if (keepSettings) state.settings = previousSettings; run = state.run; hiddenAt = null; lastFrame = performance.now(); lastThought = 0; autoUpgradeCarry = 0; combatFeedback = 'Trefferfenster erfassen.'; upgradesSignature = ''; localStorage.removeItem(LEGACY_SAVE_KEY); renderLog(); renderAchievements(); render(); thoughts(); save(false); $('saveStatus').textContent = 'NEUER LAUF'; setTimeout(() => $('saveStatus').textContent = 'AUTOSAVE AKTIV', 900); return debugStatus(); } 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 checkImpulseMilestoneAchievements() { const total = run.stats.totalImpulses || 0; impulseMilestoneAchievements.forEach(achievement => { if (total >= achievement.impulseThreshold) unlockAchievement(achievement.id); }); } 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 = Math.min(PROGRAM_LIMITS.resources.impulses, run.impulses + gained); run.stats.totalImpulses = (run.stats.totalImpulses || 0) + gained; checkImpulseMilestoneAchievements(); } 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 Math.min(PROGRAM_LIMITS.rates.impulses, (run.autoRate + hardwareRate) * achievementMultiplier('impulse') * effectiveSynthesisScale()); } function effectiveClickPower() { return run.clickPower * (componentOnline('memory') ? 1.15 : 1); } function effectiveCycleRate() { return Math.min(PROGRAM_LIMITS.rates.cycles, run.cycleRate * achievementMultiplier('cycles') * effectiveSynthesisScale()); } function effectiveBitRate() { if (!run.upgrades.bitSynthesizer || STAGES.indexOf(run.stage) < STAGES.indexOf('subroutine')) return 0; return Math.min(PROGRAM_LIMITS.rates.bits, Math.max(0, run.bitRate || 0) * effectiveSynthesisScale()); } function effectiveByteRate() { if (!run.upgrades.byteSynthesizer || STAGES.indexOf(run.stage) < STAGES.indexOf('process')) return 0; return Math.min(PROGRAM_LIMITS.rates.bytes, 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 = Math.min(PROGRAM_LIMITS.resources.bits, 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 = Math.min(PROGRAM_LIMITS.resources.bytes, 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 repairStabilityWithImpulses() { if (STAGES.indexOf(run.stage) < STAGES.indexOf('fragment') || run.combat.active || run.stability >= 100 || run.impulses < MANUAL_REPAIR.impulseCost) return; const restored = Math.min(MANUAL_REPAIR.stability, 100 - run.stability); run.impulses -= MANUAL_REPAIR.impulseCost; run.stability = Math.min(100, run.stability + restored); addLog(`IMPULS-REPARATUR: ${MANUAL_REPAIR.impulseCost} Impulse stabilisieren ${Math.round(restored)} Strukturpunkte.`); render(); save(false); } function chargeCombatShield(type) { if (STAGES.indexOf(run.stage) < STAGES.indexOf('fragment') || run.combat.active || !['bits', 'bytes'].includes(type)) return; const isByte = type === 'bytes'; const cost = isByte ? SHIELD_RULES.byteCost : SHIELD_RULES.bitCost; const charge = isByte ? SHIELD_RULES.byteCharge : SHIELD_RULES.bitCharge; const maximum = isByte ? SHIELD_RULES.byteMax : SHIELD_RULES.bitMax; if (run.shields[type] >= maximum || freeResourceAmount(type) < cost) return; run[type] -= cost; run.shields[type] = Math.min(maximum, run.shields[type] + charge); addLog(`SCHILD GELADEN: ${cost} freie ${resourceLabel(type, cost)} werden zu ${charge} ${isByte ? 'Byte' : 'Bit'}-Schildschicht.`); render(); save(false); } 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(); showGuardedDialog($('achievementDialog')); } function reconcileAchievements() { if (run.stats.manualPulses > 0) unlockAchievement('awake'); if (run.stats.directHits > 0) unlockAchievement('bullseye'); if (run.stats.manualPulses >= 100) unlockAchievement('manual100'); checkImpulseMilestoneAchievements(); 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.securityCombatResolved) unlockAchievement('signatureBoss'); 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 (run.kernelResolved) unlockAchievement('kernel'); if (run.scaleGuardianResolved) unlockAchievement('scalingBoss'); 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 isCriticalPulseHit(event, source) { if (run.stage === 'bit') return source === 'keyboard' || isDirectBitHit(event); if (!event || $('pulseTarget').classList.contains('hidden')) return false; const rect = $('pulseTarget').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) * PULSE_AIM_RULES.criticalRadius; } function positionPulseTarget() { const core = $('core'); if (core.clientWidth && core.clientHeight) { const maxX = Math.max(0, core.clientWidth / 2 - PULSE_AIM_RULES.edgePadding); const maxY = Math.max(0, core.clientHeight / 2 - PULSE_AIM_RULES.edgePadding); pulseTargetPosition.x = Math.max(-maxX, Math.min(maxX, pulseTargetPosition.x)); pulseTargetPosition.y = Math.max(-maxY, Math.min(maxY, pulseTargetPosition.y)); } $('pulseTarget').style.setProperty('--target-x', `${pulseTargetPosition.x}px`); $('pulseTarget').style.setProperty('--target-y', `${pulseTargetPosition.y}px`); } function movePulseTarget() { if (run.stage === 'bit') return; const core = $('core'); const maxX = Math.max(0, core.clientWidth / 2 - PULSE_AIM_RULES.edgePadding); const maxY = Math.max(0, core.clientHeight / 2 - PULSE_AIM_RULES.edgePadding); let next = pulseTargetPosition; 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 - pulseTargetPosition.x, candidate.y - pulseTargetPosition.y) >= PULSE_AIM_RULES.minimumJump) break; } pulseTargetPosition = next; positionPulseTarget(); $('pulseTarget').classList.remove('target-jump'); void $('pulseTarget').offsetWidth; $('pulseTarget').classList.add('target-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 pulseSourceForTarget(target) { if (target === $('coreProgramAvatar')) return run.stage === 'program' ? 'entity' : null; if (target === $('pixelBeing') && run.stage !== 'program') return run.stage === 'bit' ? 'bit' : 'entity'; if (target === $('pulseTarget')) return run.stage !== 'bit' && run.upgrades.criticalFocus ? 'aim' : null; return null; } function pulsePointInsideTarget(target, point) { if (!target || !point) return false; const rect = target.getBoundingClientRect(); const margin = 18; return point.x >= rect.left - margin && point.x <= rect.right + margin && point.y >= rect.top - margin && point.y <= rect.bottom + margin; } function hideHoldPulseRing() { clearTimeout(holdPulseTimer); holdPulseTimer = null; clearTimeout(holdPulseFadeTimer); const ring = $('holdPulseRing'); const wasVisible = ring.classList.contains('active'); ring.classList.remove('active'); ring.classList.toggle('fading', wasVisible); if (wasVisible) holdPulseFadeTimer = setTimeout(() => ring.classList.remove('fading'), 190); } function clearPulseHoldState() { hideHoldPulseRing(); holdPulsePointerId = null; holdPulseTarget = null; holdPulseFired = false; holdPulsePoint = null; } function cancelPulseHold() { clearPulseHoldState(); } function positionHoldPulseRing() { const coreRect = $('core').getBoundingClientRect(); if ($('holdPulseRing').parentElement !== $('core')) $('core').appendChild($('holdPulseRing')); $('holdPulseRing').style.left = `${holdPulsePoint.x - coreRect.left}px`; $('holdPulseRing').style.top = `${holdPulsePoint.y - coreRect.top}px`; } function beginHoldPulseCycle() { const source = pulseSourceForTarget(holdPulseTarget); const ringActive = $('holdPulseRing').classList.contains('active'); if (!source || (!ringActive && !pulsePointInsideTarget(holdPulseTarget, holdPulsePoint)) || document.querySelector('dialog[open]')) { cancelPulseHold(); return; } if (!holdPulseFired) { clearTimeout(holdPulseFadeTimer); $('holdPulseRing').classList.remove('fading', 'active'); positionHoldPulseRing(); void $('holdPulseRing').offsetWidth; $('holdPulseRing').classList.add('active'); } holdPulseTimer = setTimeout(() => { const currentSource = pulseSourceForTarget(holdPulseTarget); if (!currentSource || !$('holdPulseRing').classList.contains('active') || document.querySelector('dialog[open]')) { cancelPulseHold(); return; } holdPulseFired = true; pulse({ clientX: holdPulsePoint.x, clientY: holdPulsePoint.y }, currentSource); beginHoldPulseCycle(); }, holdPulseFired ? HOLD_PULSE_INTERVAL_MS : HOLD_PULSE_CHARGE_MS); } function beginPulseHold(event, target) { if (!event.isPrimary || event.button !== 0 || !pulseSourceForTarget(target) || document.querySelector('dialog[open]')) return; event.preventDefault(); event.stopPropagation(); cancelPulseHold(); holdPulsePointerId = event.pointerId; holdPulseTarget = target; holdPulsePoint = { x: event.clientX, y: event.clientY }; holdPulseTimer = setTimeout(beginHoldPulseCycle, HOLD_PULSE_ARM_DELAY_MS); } function movePulseHold(event) { if (event.pointerId !== holdPulsePointerId || !holdPulseTarget) return; holdPulsePoint = { x: event.clientX, y: event.clientY }; if ($('holdPulseRing').classList.contains('active')) { positionHoldPulseRing(); return; } if (!pulsePointInsideTarget(holdPulseTarget, holdPulsePoint)) { cancelPulseHold(); return; } positionHoldPulseRing(); } function finishPulseHold(event) { if (event.pointerId !== holdPulsePointerId || !holdPulseTarget) return; const target = holdPulseTarget; const source = pulseSourceForTarget(target); const point = { x: event.clientX, y: event.clientY }; const shouldTap = !holdPulseFired && source && pulsePointInsideTarget(target, point) && !document.querySelector('dialog[open]'); clearPulseHoldState(); if (shouldTap) pulse({ clientX: point.x, clientY: point.y }, source); } function pulse(event, source = 'button') { if (run.stage === 'bit' && source !== 'bit' && source !== 'keyboard') return; const directHit = isCriticalPulseHit(event, source); const multiplier = directHit ? run.stage === 'bit' ? BIT_CHASE_RULES.directMultiplier : PULSE_AIM_RULES.criticalMultiplier : 1; const gained = effectiveClickPower() * multiplier; grantImpulses(gained); run.stats.manualPulses += gained; run.stats.hitCount++; const rect = $('core').getBoundingClientRect(); const activeTarget = run.stage === 'program' ? $('coreProgramAvatar') : $('pixelBeing'); const targetRect = activeTarget.getBoundingClientRect(); const floating = document.createElement('b'); floating.className = `float ${directHit ? 'direct-hit' : ''}`; floating.textContent = directHit ? `KRIT +${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); activeTarget.classList.remove('pulse-impact'); void activeTarget.offsetWidth; activeTarget.classList.add('pulse-impact'); setTimeout(() => activeTarget.classList.remove('pulse-impact'), 180); 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('Kritischer Kontakt. Das Signal vervielfacht sich.', true); } if (run.stats.manualPulses >= 100) unlockAchievement('manual100'); if (run.stage === 'bit') moveBit(); else if (directHit) movePulseTarget(); 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 …'; showGuardedDialog($('evolutionDialog')); 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 * (1 + bought * 0.12)); 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 === 'megabytes') return amount === 1 ? 'Megabyte' : 'Megabytes'; 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 === 'megabytes') return 'Megabytes'; 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 === 'megabytes') 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 === 'criticalFocus') return 'assets/ui/module-signal-target.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 === 'megabytes') return 'megabyte'; 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 evolutionBossGate(upgradeId) { return EVOLUTION_BOSS_GATES[upgradeId] || null; } function evolutionBossResolved(upgradeId) { if (upgradeId === 'subroutine') return run.securityCombatResolved; if (upgradeId === 'process') return run.kernelResolved; if (upgradeId === 'automateSynthesis') return run.scaleGuardianResolved; return true; } function evolutionBossPrerequisitesMet(upgradeId) { if (upgradeId === 'subroutine') return installedComponentIds().length >= 2 && run.hardwareEvent.resolved; if (upgradeId === 'process') return run.components.io && installedComponentIds().length >= 3; if (upgradeId === 'automateSynthesis') return installedComponentIds().length >= COMPONENT_IDS.length; return true; } function evolutionBossResourcesMet(upgradeId) { const gate = evolutionBossGate(upgradeId); if (!gate) return true; return Object.entries(gate.resources).every(([key, amount]) => spendableResourceAmount(key) >= amount) && run.stability >= gate.stability; } function evolutionUpgradeAvailable(upgradeId) { if (evolutionBossResolved(upgradeId)) return true; return evolutionBossPrerequisitesMet(upgradeId) && evolutionBossResourcesMet(upgradeId); } function evolutionUpgradeLockedText(upgradeId) { const gate = evolutionBossGate(upgradeId); if (!gate) return 'VORAUSSETZUNGEN FEHLEN'; if (evolutionBossResolved(upgradeId)) return 'BOSSPRÜFUNG BESTANDEN // RESSOURCEN FÜR EVOLUTION BENÖTIGT'; if (!evolutionBossPrerequisitesMet(upgradeId)) return `${gate.prerequisite} // DANACH BOSSKAMPF`; return `${gate.label} // MINDESTRESERVEN + ${gate.stability}% STABILITÄT BENÖTIGT`; } function requestEvolutionBoss(upgradeId) { const gate = evolutionBossGate(upgradeId); if (!gate || evolutionBossResolved(upgradeId)) return false; if (!evolutionBossPrerequisitesMet(upgradeId) || !evolutionBossResourcesMet(upgradeId)) return true; if (run.combat.active || run.pendingCombat.target || document.querySelector('dialog[open]')) return true; if (gate.target === 'scale') run.scaleGuardianTriggered = true; addLog(`EVOLUTIONSPRÜFUNG GESTARTET: ${gate.label} kontrolliert deine Reserven. Ein Sieg schaltet den Aufstieg frei.`, true); requestImmediateCombat(gate.target); return true; } 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(); if (run.stage === 'program' && run.autoUpgrader?.unlocked && run.autoUpgrader.enabled) return installed; ['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 === 'criticalFocus' ? `STATUS // INSTALLIERT // KRITISCHE MITTENTREFFER x${PULSE_AIM_RULES.criticalMultiplier}` : 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'; showGuardedDialog($('upgradeInfoDialog')); } function canPayResourceCost(costs) { return Object.entries(costs).every(([key, amount]) => spendableResourceAmount(key) >= 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 advancedSecurityCombat(target = run.combat.target) { return target === 'security' && run.upgrades.subroutine && run.securityCombatResolved && !run.scannerTriggered; } function combatProfile(target = run.combat.target) { return advancedSecurityCombat(target) ? ADVANCED_SECURITY_TARGET : COMBAT_TARGETS[target]; } function evolutionBossCombat(target = run.combat.target) { if (!run.combat.active || advancedSecurityCombat(target)) return false; return ['security', 'kernel', 'scale'].includes(target); } function combatBossPhase() { if (!evolutionBossCombat() || !run.combat.maxHealth) return { number: 0, label: '', strengthBonus: 0, damageBonus: 0 }; const ratio = run.combat.health / run.combat.maxHealth; if (ratio > 0.66) return { number: 1, label: 'ANALYSE', strengthBonus: 0, damageBonus: 0 }; if (ratio > 0.33) return { number: 2, label: 'GEGENMASSNAHMEN', strengthBonus: 0.12, damageBonus: 1 }; return { number: 3, label: 'KERNVERRIEGELUNG', strengthBonus: 0.25, damageBonus: 2 }; } function effectiveCombatStrength() { return run.combat.strength + combatBossPhase().strengthBonus; } function combatAmmoCost(target = run.combat.target) { const profile = combatProfile(target); const shockCost = profile?.shockCost || COMBAT_RULES.shockCost; if (combatAmmoMode === 'bytes' && freeResourceAmount('bytes') >= 1) return { bytes: 1 }; if (combatAmmoMode === 'bits' && freeResourceAmount('bits') >= 1) return { bits: 1 }; combatAmmoMode = 'impulses'; return { impulses: shockCost }; } function combatDamageForAmmo(ammo, critical = false, target = run.combat.target) { const mode = ammo.bytes ? 'bytes' : ammo.bits ? 'bits' : 'impulses'; const profile = combatProfile(target); const scale = profile?.damageScale?.[mode] ?? 1; return Math.round((COMBAT_RULES.ammoDamage[mode] + (critical ? COMBAT_RULES.ammoCriticalBonus : 0)) * scale); } function combatAmmoAssessment(mode, profile = combatProfile()) { const scale = profile?.damageScale?.[mode] ?? 1; if (scale < 0.8) return 'GEDÄMPFT'; if (scale > 1.1) return mode === 'bytes' ? 'DURCHDRINGEND' : 'EFFIZIENT'; return 'STANDARD'; } function combatDirectThreatActive(target = run.combat.target) { return target !== 'watchdog' && STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') && run.shields.bits <= 0 && run.shields.bytes <= 0; } function combatPressureText() { if (!run.combat.active || run.combat.target === 'watchdog') return 'Kein laufender Strukturangriff.'; if (run.shields.bytes > 0) return `Zielt auf geladenen Byte-Schild // ${n(run.shields.bytes)} Schicht(en)`; if (run.shields.bits > 0) return `Zielt auf geladenen Bit-Schild // ${n(run.shields.bits)} Schicht(en)`; return `Zielt auf Stabilität // ${Math.round(run.stability)}%`; } function showCombatImpact(kind, text) { const impact = $('combatImpact'); const panel = $('combatPanel'); const visual = $('combatVisual'); if (!impact || !panel || !visual) return; clearTimeout(combatImpactTimer); const impactClasses = ['player-hit', 'critical-hit', 'enemy-shield-hit', 'enemy-direct-hit', 'player-miss']; impact.classList.remove('active', ...impactClasses); panel.classList.remove('enemy-shield-impact', 'enemy-direct-impact', 'player-miss-impact'); visual.classList.remove('player-hit-impact', 'critical-hit-impact'); void impact.offsetWidth; impact.textContent = text; impact.classList.add('active', kind); if (kind === 'player-hit' || kind === 'critical-hit') visual.classList.add(`${kind}-impact`); if (kind === 'enemy-shield-hit') panel.classList.add('enemy-shield-impact'); if (kind === 'enemy-direct-hit') panel.classList.add('enemy-direct-impact'); if (kind === 'player-miss') panel.classList.add('player-miss-impact'); combatImpactTimer = setTimeout(() => { impact.classList.remove('active', ...impactClasses); panel.classList.remove('enemy-shield-impact', 'enemy-direct-impact', 'player-miss-impact'); visual.classList.remove('player-hit-impact', 'critical-hit-impact'); }, 560); } function minimumForCurrentStructure() { return STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') ? STRUCTURAL_MINIMUMS.fragment : { bits: 0, bytes: run.stage === 'byte' ? 1 : 0 }; } function recalculateProductionForStage() { const stageIndex = STAGES.indexOf(run.stage); const fragmentActive = stageIndex >= STAGES.indexOf('fragment'); const subroutineActive = stageIndex >= STAGES.indexOf('subroutine'); const processActive = stageIndex >= STAGES.indexOf('process'); run.autoRate = (run.upgrades.trickle ? 0.15 : 0) + (fragmentActive && run.upgrades.collector ? 0.5 : 0) + (fragmentActive && run.upgrades.pulseRouter ? 0.3 : 0) + (fragmentActive && run.upgrades.byteCapacitor ? 0.45 : 0) + (subroutineActive && run.upgrades.throughputKernel ? 0.7 : 0) + (subroutineActive ? (run.upgrades.replicate2 || 0) * 0.25 : 0) + (fragmentActive && run.scannerChoice === 'copy' ? 0.35 : 0); run.clickPower = 1 + (fragmentActive && run.upgrades.amplifier ? 1 : 0) + (fragmentActive && run.upgrades.signalFocus ? 1 : 0) + (fragmentActive && run.upgrades.byteCapacitor ? 1 : 0); run.cycleRate = processActive ? 0.45 : subroutineActive ? 0.2 + (componentOnline('io') ? 0.08 : 0) : 0; run.bitRate = subroutineActive ? (run.upgrades.bitSynthesizer || 0) * 0.04 : 0; run.byteRate = processActive ? (run.upgrades.byteSynthesizer || 0) * 0.04 : 0; run.synthesisScale = run.stage === 'program' && run.upgrades.automateSynthesis ? 10 : 1; } function applyCombatDowngrade(recovery) { const targetIndex = STAGES.indexOf(recovery.to); if (recovery.to === 'bit') { state.run = initialState().run; run = state.run; bitPosition = { x: 0, y: 0 }; pulseTargetPosition = { x: 64, y: -48 }; return; } run.stage = recovery.to; run.stability = 45; run.shields = { bits: 0, bytes: 0 }; run.combat = emptyCombat(); run.pendingCombat = emptyPendingCombat(); run.evolutionRest = 0; scannerOpening = false; scannerViewOpen = false; selectedComponent = null; combatPressureCarry = 0; lastPendingCombatSecond = null; if (targetIndex < STAGES.indexOf('program')) { run.upgrades.automateSynthesis = false; run.autoUpgrader = { unlocked: false, enabled: true, purchases: 0, lastPurchase: null }; } if (targetIndex < STAGES.indexOf('process')) { run.upgrades.process = false; run.scaleGuardianTriggered = false; run.scaleGuardianResolved = false; run.scaleGuardianRetryAt = 0; } if (targetIndex < STAGES.indexOf('subroutine')) run.upgrades.subroutine = false; if (targetIndex < STAGES.indexOf('fragment')) run.upgrades.collector = false; if (recovery.to === 'byte') run.bytes = Math.max(1, run.bytes); applyStructuralMinimums(run); recalculateProductionForStage(); } function restartAfterStructuralCollapse() { $('structuralCollapseDialog').close(); if (pendingCombatDowngrade) { const recovery = pendingCombatDowngrade; pendingCombatDowngrade = null; state.meta.deaths++; applyCombatDowngrade(recovery); upgradesSignature = ''; addLog(`KAMPFNIEDERLAGE: ${recovery.fromLabel} zerfällt. Die überlebende Struktur fällt auf ${recovery.toLabel} zurück.`, true); unlockAchievement('firstDeath'); renderLog(); render(); thoughts(); save(); return; } state.meta.deaths++; state.run = initialState().run; run = state.run; bitPosition = { x: 0, y: 0 }; pulseTargetPosition = { x: 64, y: -48 }; upgradesSignature = ''; addLog('Ein instabiles Muster ist kollabiert. Ein einzelner Restzustand reagiert erneut.', true); unlockAchievement('firstDeath'); renderLog(); render(); thoughts(); save(); } function showStructuralCollapse(message) { const stageIndex = STAGES.indexOf(run.stage); const combatDefeat = run.combat.active && stageIndex > 0; pendingCombatDowngrade = combatDefeat ? { from: run.stage, to: STAGES[stageIndex - 1], fromLabel: evolutionName(), toLabel: (phaseLabels[STAGES[stageIndex - 1]] || STAGES[stageIndex - 1]).replace(/^PHASE \d+ \/\/ /, '') } : null; run.combat.active = false; scannerOpening = false; if ($('eventDialog').open) $('eventDialog').close(); $('structuralCollapseTitle').textContent = combatDefeat ? 'EVOLUTIONSSCHICHT ZERFALLEN' : 'RESTZUSTAND'; $('structuralCollapseText').textContent = message; $('structuralCollapseConsequence').textContent = combatDefeat ? `Die Grundstruktur überlebt. Rückfall: ${pendingCombatDowngrade.fromLabel} → ${pendingCombatDowngrade.toLabel}.` : 'Ohne Backup bleibt nur ein neuer Anfang.'; $('structuralRestartButton').textContent = combatDefeat ? 'VORHERIGE STUFE REKONSTRUIEREN' : 'RESTSIGNAL AKTIVIEREN'; showGuardedDialog($('structuralCollapseDialog')); 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)) { if (run.combat.active && run.stability <= 0) { showStructuralCollapse(`${reason}: Die gegnerische Rückkopplung zerreißt die aktive Subroutine.`); return false; } 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 upgradeAtCap(upgrade) { const cap = PROGRAM_LIMITS.upgradeLevels[upgrade.id]; return Number.isFinite(cap) && (run.upgrades[upgrade.id] || 0) >= cap; } function buy(upgrade, { automatic = false } = {}) { if (!automatic && requestEvolutionBoss(upgrade.id)) return false; const costs = upgradeResourceCost(upgrade); if (upgradeAtCap(upgrade) || !canPayResourceCost(costs) || (upgrade.available && !upgrade.available()) || (upgrade.once && run.upgrades[upgrade.id])) return false; const purchaseLog = (text, story = true) => { if (!automatic) addLog(text, story); }; 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'; thoughts(); resetBitPriceCurve(); beginByteTrial(); addLog('Acht Zustände verbinden sich. Ich kann mich erinnern.', true); unlockAchievement('byte'); } if (upgrade.id === 'criticalFocus') { run.upgrades.criticalFocus = true; addLog('SIGNALPEILUNG: Eine bewegliche Zielmarkierung folgt der aktiven Impulsaufnahme.', true); } if (upgrade.id === 'collector') { run.autoRate += 0.5; run.upgrades.collector = true; run.stage = 'fragment'; thoughts(); 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'; thoughts(); 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; purchaseLog(`BIT-SYNTHESE STUFE ${run.upgrades.bitSynthesizer}: Freie Rechenfenster schreiben schneller neue Bits in die Strukturreserve.`); } if (upgrade.id === 'process') { run.cycleRate = Math.max(run.cycleRate, 0.45); run.upgrades.process = true; run.stage = 'process'; thoughts(); 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'; thoughts(); 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; purchaseLog('BIT-BLOCK: 10 freie Bits wurden aus Rechenzeit gebündelt.'); } if (upgrade.id === 'cycleByteBlock') { run.bytes += 10; run.upgrades.cycleByteBlock = (run.upgrades.cycleByteBlock || 0) + 1; purchaseLog('BYTE-BLOCK: 10 freie Bytes wurden aus Rechenzeit kompiliert.'); } if (upgrade.id === 'byteSynthesizer') { run.byteRate += 0.04; run.upgrades.byteSynthesizer = (run.upgrades.byteSynthesizer || 0) + 1; purchaseLog(`BYTE-SYNTHESE STUFE ${run.upgrades.byteSynthesizer}: Der Prozess ordnet freie Bits zu neuen Speicherblöcken.`); } if (upgrade.id === 'kilobyteCompiler') { run.kilobytes++; run.upgrades.kilobyteCompiler = (run.upgrades.kilobyteCompiler || 0) + 1; purchaseLog(`KILOBYTE-SEGMENT: ${DATA_SCALE.bytesPerKilobyte} freie Bytes verdichten sich zu einem größeren Speicherblock.`); } 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; purchaseLog('BIT-PUFFER: Ein weiteres Bit stabilisiert die eigene Struktur.'); } if (upgrade.id === 'byteBuffer') { run.bytes++; run.upgrades.byteBuffer = (run.upgrades.byteBuffer || 0) + 1; purchaseLog('BYTE-RESERVE: Ein zusätzlicher Speicherblock wird als Körperreserve gebunden.'); } capProgramResources(); upgradesSignature = ''; render(); save(false); return true; } function capProgramResources() { Object.entries(PROGRAM_LIMITS.resources).forEach(([key, maximum]) => { run[key] = Math.min(maximum, Math.max(0, finite(run[key], 0, 0, maximum))); if (['bits', 'bytes', 'kilobytes', 'megabytes'].includes(key)) run[key] = Math.floor(run[key]); }); } function autoUpgradeCandidate() { const priority = ['byteSynthesizer', 'bitSynthesizer', 'replicate2', 'kilobyteCompiler', 'cycleByteBlock', 'cycleBitBlock', 'byteBuffer', 'bitBuffer']; return priority.map(id => upgrades.find(upgrade => upgrade.id === id)).find(upgrade => { if (!upgrade || upgradeAtCap(upgrade)) return false; if (upgrade.id === 'kilobyteCompiler' && run.megabytes >= PROGRAM_LIMITS.resources.megabytes) return false; if (upgrade.id === 'kilobyteCompiler' && freeResourceAmount('bytes') < DATA_SCALE.bytesPerKilobyte + PROGRAM_LIMITS.byteReserve) return false; if (upgrade.id === 'byteBuffer' && freeResourceAmount('bits') < upgrade.cost + PROGRAM_LIMITS.bitReserve) return false; const costs = upgradeResourceCost(upgrade); return canPayResourceCost(costs) && (!upgrade.available || upgrade.available()); }) || null; } function unlockAutoUpgrader() { if (run.autoUpgrader.unlocked || run.stage !== 'program' || run.kilobytes < AUTO_UPGRADER_UNLOCK_KILOBYTES) return false; run.autoUpgrader.unlocked = true; run.autoUpgrader.enabled = true; upgradesSignature = ''; addLog(`AUTOUPGRADER FREIGESCHALTET: ${AUTO_UPGRADER_UNLOCK_KILOBYTES} KB bilden eine selbstverwaltete Produktionsschicht. Wiederholbare Module werden jetzt automatisch bis zu ihren sicheren Obergrenzen ausgebaut.`, true); save(false); return true; } function bundleMegabytes() { if (!run.autoUpgrader.unlocked || run.megabytes >= PROGRAM_LIMITS.resources.megabytes) return 0; const bundles = Math.min( Math.floor(run.kilobytes / DATA_SCALE.kilobytesPerMegabyte), PROGRAM_LIMITS.resources.megabytes - run.megabytes ); if (bundles <= 0) return 0; run.kilobytes -= bundles * DATA_SCALE.kilobytesPerMegabyte; const firstMegabyte = run.megabytes === 0; run.megabytes += bundles; if (firstMegabyte) addLog(`MEGABYTE ERREICHT: ${DATA_SCALE.kilobytesPerMegabyte} KB-Segmente wurden zu einem zusammenhängenden MB-Block gebündelt.`, true); return bundles; } function updateAutoUpgrader(seconds) { if (run.stage !== 'program') { autoUpgradeCarry = 0; return; } unlockAutoUpgrader(); if (!run.autoUpgrader.unlocked) return; bundleMegabytes(); if (!run.autoUpgrader.enabled) return; autoUpgradeCarry += seconds; if (autoUpgradeCarry < AUTO_UPGRADER_INTERVAL) return; autoUpgradeCarry %= AUTO_UPGRADER_INTERVAL; const candidate = autoUpgradeCandidate(); if (!candidate || !buy(candidate, { automatic: true })) return; run.autoUpgrader.purchases++; run.autoUpgrader.lastPurchase = candidate.name; bundleMegabytes(); capProgramResources(); upgradesSignature = ''; save(false); } function renderAutoUpgrader() { const console = $('autoUpgraderConsole'); const inProgram = run.stage === 'program'; console.classList.toggle('hidden', !inProgram); if (!inProgram) return; const unlocked = run.autoUpgrader.unlocked; $('autoUpgraderState').textContent = unlocked ? run.autoUpgrader.enabled ? 'AKTIV' : 'PAUSIERT' : `GESPERRT // ${Math.min(AUTO_UPGRADER_UNLOCK_KILOBYTES, run.kilobytes)} / ${AUTO_UPGRADER_UNLOCK_KILOBYTES} KB`; $('autoUpgraderToggle').classList.toggle('hidden', !unlocked); $('autoUpgraderToggle').textContent = run.autoUpgrader.enabled ? 'AUTOMATIK PAUSIEREN' : 'AUTOMATIK STARTEN'; $('autoUpgraderToggle').setAttribute('aria-pressed', String(run.autoUpgrader.enabled)); const candidate = unlocked && run.autoUpgrader.enabled ? autoUpgradeCandidate() : null; $('autoUpgraderQueue').textContent = !unlocked ? `Noch ${Math.max(0, AUTO_UPGRADER_UNLOCK_KILOBYTES - run.kilobytes)} KB bis zur selbstverwalteten Produktionsschicht.` : !run.autoUpgrader.enabled ? 'Die Produktionsschicht wartet auf Freigabe.' : candidate ? `NÄCHSTER AUSBAU // ${candidate.name}` : 'WARTET // Ressourcen werden gesammelt oder alle Ausbaugrenzen sind erreicht.'; $('autoUpgraderDetail').textContent = unlocked ? `${run.autoUpgrader.purchases} automatische Käufe // ${DATA_SCALE.kilobytesPerMegabyte} KB = 1 MB // max. ${PROGRAM_LIMITS.rates.impulses} Impulse/s, ${PROGRAM_LIMITS.rates.cycles} Zyklen/s // ${PROGRAM_LIMITS.resources.megabytes} MB` : 'Bei 20 KB übernimmt das Programm wiederholbare Produktionsmodule. Einmalige Story- und Fähigkeitsmodule bleiben sichtbar.'; } function renderUpgrades() { const evolutionGoalId = evolutionUpgradeByStage[run.stage] || ''; const resourcePathPriority = { bitBuffer: 1, byteBuffer: 2, replicate2: 3, kilobyteCompiler: 4 }; const visible = upgrades .filter(upgrade => (upgrade.show() || upgrade.id === evolutionGoalId) && !(run.stage === 'program' && run.autoUpgrader?.unlocked && run.autoUpgrader.enabled && AUTO_UPGRADE_IDS.includes(upgrade.id))) .sort((left, right) => { const leftPriority = left.id === evolutionGoalId ? 0 : resourcePathPriority[left.id] ?? 10; const rightPriority = right.id === evolutionGoalId ? 0 : resourcePathPriority[right.id] ?? 10; return leftPriority - rightPriority; }); const installed = installedVisibleUpgrades(); const signature = JSON.stringify(visible.map(upgrade => [ upgrade.id, upgrade.id === evolutionGoalId, evolutionBossResolved(upgrade.id), evolutionBossPrerequisitesMet(upgrade.id), evolutionBossResourcesMet(upgrade.id), Math.round(run.stability), 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); const hasInstalledModules = installedButtons.length > 0; $('installedUpgradeDock').classList.toggle('hidden', !hasInstalledModules); $('installedUpgradeStatus').classList.toggle('hidden', !hasInstalledModules); $('statusStrip').classList.toggle('has-modules', hasInstalledModules); 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 evolutionGoal = upgrade.id === evolutionGoalId; const bossGate = evolutionGoal ? evolutionBossGate(upgrade.id) : null; const bossPending = Boolean(bossGate && !evolutionBossResolved(upgrade.id)); const requirementsMet = upgrade.available ? upgrade.available() : true; const costs = upgradeResourceCost(upgrade); const displayCosts = bossPending ? bossGate.resources : costs; const costLabel = formatResourceCost(displayCosts).toUpperCase(); const affordable = bossPending ? evolutionBossResourcesMet(upgrade.id) : canPayResourceCost(costs); const atCap = upgradeAtCap(upgrade); const canBuy = !owned && !atCap && affordable && requirementsMet; button.className = `upgrade ${owned ? 'owned' : ''} ${canBuy ? 'available' : ''} ${run.stage === 'bit' ? 'early-path' : ''} ${evolutionGoal ? 'evolution-goal' : ''} ${bossPending ? 'boss-gate' : ''}`; button.disabled = !canBuy; title.textContent = bossPending ? `BOSSKAMPF // ${bossGate.label}` : `${owned ? '✓ ' : ''}${upgrade.name}`; description.textContent = bossPending ? `${upgrade.text} Die angezeigten Werte sind Mindestreserven und werden beim Start nicht pauschal abgezogen.` : upgrade.text; if (owned) { price.textContent = 'INSTALLIERT'; } else { const lockedText = upgradeLockedText(upgrade); const bossNote = bossPending ? !evolutionBossPrerequisitesMet(upgrade.id) ? `${bossGate.prerequisite} // STABILITÄT ${Math.round(run.stability)} / ${bossGate.stability}%` : canBuy ? `KAMPF BEREIT // STABILITÄT ${Math.round(run.stability)} / ${bossGate.stability}% // KLICKEN ZUM START` : `MINDESTRESERVEN FEHLEN // STABILITÄT ${Math.round(run.stability)} / ${bossGate.stability}%` : ''; renderUpgradeCost(price, displayCosts, atCap ? `OBERGRENZE // STUFE ${PROGRAM_LIMITS.upgradeLevels[upgrade.id]}` : bossNote || (!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.remove('hidden'); 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 ? 'SIGNATUR-PRÜFER: BESIEGT' : securityFight ? `BOSS // ${Math.ceil(run.combat.health)} INTEGRITÄT` : securityKnown ? 'BOSSPRÜFUNG BEREITEN' : '?'; 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 ? 'LAUFZEIT-ARBITER: BESIEGT' : kernelFight ? `ARBITER // ${Math.ceil(run.combat.health)} INTEGRITÄT` : kernelKnown ? 'PROZESSPRÜFUNG' : '?'; 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 ? 'SKALIERUNGS-SENTINEL: BESIEGT' : scaleFight ? `SENTINEL // ${Math.ceil(run.combat.health)} INTEGRITÄT` : scaleKnown ? 'PROGRAMMPRÜFUNG' : '?'; const signalSignature = JSON.stringify([ run.stage, run.byteTrial.resolved, run.parasiteResolved, run.environmentScanner, Boolean(run.firstComponent), run.hardwareEvent.resolved, installedComponentIds().length, run.proposal.resolved, run.securityCombatResolved, run.kernelResolved, run.scaleGuardianResolved ]); if (developmentSignalSignature && developmentSignalSignature !== signalSignature) showDevelopmentSignal(); developmentSignalSignature = signalSignature; } function showDevelopmentSignal() { const toast = $('developmentSignalToast'); if (!toast) return; clearTimeout(developmentSignalTimer); $('developmentSignalTitle').textContent = `${evolutionName()} // STATUS AKTUALISIERT`; $('developmentSignalDetail').textContent = `NÄCHSTES ZIEL // ${$('nextStage').textContent} // ${$('nextStageHint').textContent}`; toast.classList.remove('visible'); void toast.offsetWidth; toast.classList.add('visible'); developmentSignalTimer = setTimeout(() => toast.classList.remove('visible'), 5000); } function renderEnvironmentScanner() { const availableInStage = STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment'); $('scannerConsole').classList.toggle('hidden', !run.environmentScanner || !availableInStage); if (!run.environmentScanner || !availableInStage) 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 Programmprüfung wartet im Evolutionsziel.' : '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 && STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') && 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. Die Programmprüfung ist jetzt im Evolutionsziel vorbereitet.' : '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 ? 'BOSS BESIEGT' : installedCount >= 2 ? 'PRÜFUNG OFFEN' : '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 programVoiceKey(archetype = programArchetype()) { if (archetype.primary === 'neutral' || archetype.primary === 'hybrid') return archetype.primary; return archetype.className.includes('program-hybrid') ? 'hybrid' : archetype.primary; } function programActionResponse(action, archetype = programArchetype()) { const voice = programMessages.actionsByAlignment?.[programVoiceKey(archetype)] || programMessages.actionsByAlignment?.hybrid; return voice?.[action] || programMessages.actions[action] || 'Ich habe deine Entscheidung registriert.'; } 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.'; const archetype = programArchetype(); if (care.lastAction) return programActionResponse(care.lastAction, archetype); 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(); showGuardedDialog($('programDialog')); 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; const response = programActionResponse(action); addLog(`PROGRAMMREAKTION: ${response}`, true); displayThought(response); 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 || $('saveMenuDialog').open; } function openStatsDialog() { render(); showGuardedDialog($('statsDialog')); lastFrame = performance.now(); } function closeStatsDialog() { $('statsDialog').close(); lastFrame = performance.now(); render(); } function renderSaveMenu() { const list = $('saveSlotList'); if (!list) return; const slots = readManualSaveSlots(); if (!slots.length) { const empty = document.createElement('p'); empty.className = 'save-slot-empty'; empty.textContent = 'Noch keine manuellen Speicherstände.'; list.replaceChildren(empty); return; } const entries = slots.map(slot => { const summary = manualSlotSummary(slot); const article = document.createElement('article'); const meta = document.createElement('div'); const title = document.createElement('strong'); const details = document.createElement('span'); const resources = document.createElement('small'); const actions = document.createElement('div'); const loadButton = document.createElement('button'); const deleteButton = document.createElement('button'); article.className = 'save-slot'; title.textContent = summary.name; details.textContent = `${summary.phase.toUpperCase()} // ${summary.runtime} // ${summary.createdAt}`; resources.textContent = summary.resources; meta.append(title, details, resources); actions.className = 'save-slot-actions'; loadButton.type = 'button'; loadButton.textContent = 'LADEN'; loadButton.addEventListener('click', () => loadManualSlot(slot.label)); deleteButton.type = 'button'; deleteButton.textContent = 'LÖSCHEN'; deleteButton.addEventListener('click', () => { if (confirm(`Speicherstand "${slot.label}" löschen?`)) deleteManualSlot(slot.label); }); actions.append(loadButton, deleteButton); article.append(meta, actions); return article; }); list.replaceChildren(...entries); } function openSaveMenu() { renderSaveMenu(); showGuardedDialog($('saveMenuDialog')); lastFrame = performance.now(); } function closeSaveMenu() { $('saveMenuDialog').close(); lastFrame = performance.now(); render(); } function promptManualSaveSlot() { const fallback = `${evolutionName()} ${time(run.elapsed)}`; const label = prompt('Name für diesen Speicherstand:', fallback); if (label === null) return null; return saveManualSlot(label); } function confirmRestartGame() { if (!confirm('Aktuellen Autosave zurücksetzen und ein neues Spiel beginnen? Manuelle Slots bleiben erhalten.')) return null; return restartGame(); } 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(() => scheduleCombat('watchdog', 'KOHÄRENZVERLUST', 0), 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; showGuardedDialog($('watchdogIntroDialog')); 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(); showGuardedDialog($('byteStoryDialog')); } 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.`; showGuardedDialog($('watchdogDialog')); } 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; showGuardedDialog($('byteDeathDialog')); tone(60, 0.6); } function restartAfterByteDeath() { $('byteDeathDialog').close(); state.meta.deaths++; state.run = initialState().run; run = state.run; bitPosition = { x: 0, y: 0 }; pulseTargetPosition = { x: 64, y: -48 }; 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; scheduleCombat('watchdog', 'ABGELAUFENER RESET-TIMER', 0); } } 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); const criticalFocusActive = run.stage !== 'bit' && run.upgrades.criticalFocus; $('pulseTarget').classList.toggle('hidden', !criticalFocusActive); positionPulseTarget(); 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}.`); $('pulseHint').classList.toggle('chase-hint', chaseActive || criticalFocusActive); $('pulseActionText').textContent = chaseActive ? 'KLICKE ODER HALTE DAS BIT' : criticalFocusActive ? 'HALTE UND FOLGE DEM SIGNALPUNKT' : 'KLICKE ODER HALTE DIE KI-GESTALT'; 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); $('megabytes').textContent = n(run.megabytes || 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)} // ${String((HOLD_PULSE_ARM_DELAY_MS + HOLD_PULSE_CHARGE_MS) / 1000).replace('.', ',')} s halten` : run.upgrades.criticalFocus ? `Treffer +${n(clickPower)} // KRIT +${n(clickPower * PULSE_AIM_RULES.criticalMultiplier)} // ${String((HOLD_PULSE_ARM_DELAY_MS + HOLD_PULSE_CHARGE_MS) / 1000).replace('.', ',')} s halten` : `Treffer +${n(clickPower)} // ${String((HOLD_PULSE_ARM_DELAY_MS + HOLD_PULSE_CHARGE_MS) / 1000).replace('.', ',')} s halten: Auto-Takt`; $('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'; const defenseAvailable = STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment'); $('manualDefenseConsole').classList.toggle('hidden', !defenseAvailable); if (defenseAvailable) { const repairButton = $('stabilityRepairButton'); const repairPossible = !run.combat.active && run.stability < 100 && run.impulses >= MANUAL_REPAIR.impulseCost; repairButton.disabled = !repairPossible; repairButton.querySelector('strong').textContent = run.combat.active ? 'REPARATUR IM KAMPF GESPERRT' : run.stability >= 100 ? 'STABILITÄT VOLL' : 'MIT IMPULSEN REPARIEREN'; repairButton.querySelector('small').textContent = `${MANUAL_REPAIR.impulseCost} Impulse // +${MANUAL_REPAIR.stability} Stabilität`; $('preparedByteShield').textContent = `${n(run.shields.bytes)} / ${SHIELD_RULES.byteMax}`; $('preparedBitShield').textContent = `${n(run.shields.bits)} / ${SHIELD_RULES.bitMax}`; const byteButton = document.querySelector('[data-shield-charge="bytes"]'); const bitButton = document.querySelector('[data-shield-charge="bits"]'); byteButton.disabled = run.combat.active || run.shields.bytes >= SHIELD_RULES.byteMax || freeResourceAmount('bytes') < SHIELD_RULES.byteCost; bitButton.disabled = run.combat.active || run.shields.bits >= SHIELD_RULES.bitMax || freeResourceAmount('bits') < SHIELD_RULES.bitCost; } $('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')); $('megabytesResource').classList.toggle('locked', !run.autoUpgrader.unlocked); $('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' ? `Strukturpfad: Impulse → Bit-Puffer → 8 freie Bits → Byte-Reserve. Fundament: ${installedComponentIds().length}/2 Softwarekomponenten // Systemhürde ${run.hardwareEvent.resolved ? 'abgeschlossen' : 'offen'} // Signatur-Prüfer ${run.securityCombatResolved ? 'besiegt' : 'wartet'}.` : run.stage === 'subroutine' ? `Strukturpfad: Impulse → Bits → Bytes. Prozessfundament: I/O ${run.components.io ? 'aktiv' : 'fehlt'} // ${installedComponentIds().length}/3 Softwarekomponenten // Laufzeit-Arbiter ${run.kernelResolved ? 'besiegt' : 'wartet'}.` : run.stage === 'process' ? `Strukturpfad: Impulse → Bits → Bytes → Kilobytes. Programmkern: ${installedComponentIds().length}/4 Softwarekomponenten // Skalierungs-Sentinel ${run.scaleGuardianResolved ? 'besiegt' : 'wartet'} // Bosswerte im Evolutionsziel.` : run.stage === 'program' ? run.autoUpgrader.unlocked ? `Datenskalierung: ${n(run.megabytes)} / ${PROGRAM_LIMITS.resources.megabytes} MB // Autoupgrader ${run.autoUpgrader.enabled ? 'aktiv' : 'pausiert'} // Raten und Speicher technisch begrenzt.` : `Autoupgrader: ${n(run.kilobytes)} / ${AUTO_UPGRADER_UNLOCK_KILOBYTES} KB // danach werden wiederholbare Produktionsmodule automatisch verwaltet.` : 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)}.` : run.byteTrial.resolved ? `Watchdog überstanden // sammle ${n(run.bytes)}/${FRAGMENT_REQUIREMENTS.bytes} Bytes für die Energieroutine.` : `Register ${Math.round(run.byteTrial.sync)}% // Watchdog ${Math.ceil(run.byteTrial.remaining)} S.` : defaultHint; $('soundButton').textContent = `SND: ${state.settings.sound ? 'AN' : 'AUS'}`; renderUpgrades(); renderAutoUpgrader(); renderProgress(); renderEnvironmentScanner(); renderScannerStage(); renderCare(); renderProgramConsole(); renderEncounterWarning(); 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); showGuardedDialog($('parasiteDialog')); 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'); $('componentConsequence').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 macht die Prüfung durch den Laufzeit-Arbiter im Evolutionsziel sichtbar.' : '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'); }); showGuardedDialog($('componentDialog')); 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'); const installedCount = installedComponentIds().length; const consequence = installedCount === 1 && !run.securityCombatResolved ? 'Zwei kontrollierte Softwareschichten machen den Signatur-Prüfer sichtbar. Du bestimmst danach über das Evolutionsziel selbst den Kampfbeginn.' : installedCount === 2 && id === 'io' && STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') && !run.kernelResolved ? 'Der I/O-Kontroller macht den Laufzeit-Arbiter sichtbar. Du bestimmst danach über das Evolutionsziel selbst den Kampfbeginn.' : installedCount === 3 && STAGES.indexOf(run.stage) >= STAGES.indexOf('process') && !run.scaleGuardianResolved ? 'Das vollständige Softwarenetz macht den Skalierungs-Sentinel sichtbar. Du bestimmst danach über das Evolutionsziel selbst den Kampfbeginn.' : null; $('componentConsequence').classList.toggle('hidden', !consequence); if (consequence) $('componentConsequenceText').textContent = consequence; 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(); } function triggerHardwareEvent(force = false) { const event = run.hardwareEvent; if (!event.component || event.resolved || (!force && run.elapsed < event.dueAt)) return; if (run.evolutionRest > 0 || run.pendingCombat.target || 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); showGuardedDialog($('hardwareEventDialog')); 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 combatWarningText(target) { return { security: 'Der Signatur-Prüfer verlangt den Nachweis einer belastbaren Softwarestruktur, bevor eine eigene Subroutine entstehen darf.', kernel: 'Der Laufzeit-Arbiter verweigert deiner Subroutine einen eigenen Schedulerbereich und fordert eine vollständige Prozessprüfung.', scale: 'Der Skalierungs-Sentinel erkennt die geplante Automatisierung als Kontrollverlust und verriegelt den neuen Datenmaßstab.', watchdog: 'Der Reset-Timer ist abgelaufen. Die gebündelte Notladung ist deine letzte Möglichkeit, das Byte zu erhalten.' }[target] || 'Eine fremde Routine bereitet Gegenmaßnahmen vor.'; } function scheduleCombat(target, source = null, seconds = COMBAT_WARNING_SECONDS[target] ?? 8) { if (!COMBAT_TARGETS[target] || run.combat.active || run.pendingCombat.target) return false; run.pendingCombat = { target, remaining: seconds, source }; lastPendingCombatSecond = Math.ceil(seconds); if (target === 'scale') run.scaleGuardianTriggered = true; addLog(`KONFRONTATION ANGEKÜNDIGT: ${COMBAT_TARGETS[target].name} reagiert in ${seconds} Sekunden. Nur geladene Schildschichten schützen vor seinem Angriff.`, true); render(); save(false); return true; } function renderEncounterWarning() { const pending = run.pendingCombat; const visible = Boolean(pending.target && COMBAT_TARGETS[pending.target]); $('encounterWarning').classList.toggle('hidden', !visible); if (!visible) return; const profile = COMBAT_TARGETS[pending.target]; const remaining = Math.max(0, pending.remaining); $('encounterCountdown').textContent = remaining > 0 ? Math.ceil(remaining) : 'BEREIT'; $('encounterWarningTitle').textContent = `${profile.name} // KONFRONTATION`; $('encounterWarningText').textContent = combatWarningText(pending.target); $('encounterResourceHint').textContent = `GELADENER SCHILD // ${n(run.shields.bytes)} Byte → ${n(run.shields.bits)} Bit → ${Math.round(run.stability)}% Stabilität // MUNITION FREI // ${n(freeResourceAmount('bytes'))} Byte + ${n(freeResourceAmount('bits'))} Bit`; $('encounterStartButton').textContent = remaining > 0 ? 'KAMPF JETZT BEGINNEN' : 'KONFRONTATION STARTEN'; } function renderCombatTutorial(target) { const profile = combatProfile(target) || COMBAT_TARGETS.security; const impulseCost = profile.shockCost || COMBAT_RULES.shockCost; const restricted = target === 'watchdog'; $('combatTutorialTitle').textContent = `${profile.name} // KAMPFGRUNDLAGEN`; const intro = advancedSecurityCombat(target) ? `Der zweite Scan hat deine Stromstoß-Signatur gelernt. Reine Impulse werden gedämpft; freie Bits und Bytes durchdringen seine adaptive Hülle. Nur zuvor geladene Schildschichten fangen Gegenangriffe ab.` : `Wähle eine einzelne Munitionsgröße gegen ${profile.name}. Impulse sind schwach; freie Bits und Bytes bleiben Munition. Nur der zuvor geladene Kampfschild schützt deine Stabilität.`; $('combatTutorialIntro').textContent = profile.repairRate ? `${intro} SELBSTREPARATUR: Bleibt ein Treffer ${String(profile.repairDelay).replace('.', ',')} Sekunden aus, heilt der Gegner ${String(profile.repairRate).replace('.', ',')} Integrität pro Sekunde. Jeder Treffer setzt die Sperre zurück.` : intro; $('tutorialImpulseAmmo').textContent = `${impulseCost} IMPULSE // ${combatDamageForAmmo({ impulses: impulseCost }, false, target)} SCHADEN // ${combatAmmoAssessment('impulses', profile)}`; $('tutorialBitAmmo').textContent = restricted ? 'BIT-STOSS // IN DIESEM NOTKAMPF GESPERRT' : `1 FREIES BIT // ${combatDamageForAmmo({ bits: 1 }, false, target)} SCHADEN // ${combatAmmoAssessment('bits', profile)}`; $('tutorialByteAmmo').textContent = restricted ? 'BYTE-STOSS // IN DIESEM NOTKAMPF GESPERRT' : `1 FREIES BYTE // ${combatDamageForAmmo({ bytes: 1 }, false, target)} SCHADEN // ${combatAmmoAssessment('bytes', profile)}`; $('tutorialDefenseOrder').textContent = 'GELADENER BYTE-SCHILD → BIT-SCHILD → STABILITÄT'; $('combatTutorialContinue').textContent = combatTutorialStartMode ? 'VERSTANDEN // KAMPF BEGINNEN' : 'VERSTANDEN // ZURÜCK'; } function openCombatTutorial(target = run.pendingCombat.target || run.combat.target || 'security', startMode = null) { if ($('combatTutorialDialog').open) return; combatTutorialTarget = COMBAT_TARGETS[target] ? target : 'security'; combatTutorialStartMode = startMode; renderCombatTutorial(combatTutorialTarget); showGuardedDialog($('combatTutorialDialog')); } function completeCombatTutorial() { const target = combatTutorialTarget; const startMode = combatTutorialStartMode; state.meta.combatTutorialSeen = true; combatTutorialTarget = null; combatTutorialStartMode = null; $('combatTutorialDialog').close(); save(false); if (startMode === 'pending') startPendingCombat(); else if (startMode === 'immediate') startCombat(target); } function requestImmediateCombat(target) { if (!state.meta.combatTutorialSeen || advancedSecurityCombat(target)) { openCombatTutorial(target, 'immediate'); return; } startCombat(target); } function startPendingCombat() { const target = run.pendingCombat.target; if (!target) return; if (!state.meta.combatTutorialSeen) { openCombatTutorial(target, 'pending'); return; } run.pendingCombat = emptyPendingCombat(); lastPendingCombatSecond = null; startCombat(target); } function updatePendingCombat(seconds) { if (!run.pendingCombat.target || run.combat.active || seconds <= 0) return; if (!document.querySelector('dialog[open]')) { run.pendingCombat.remaining = Math.max(0, run.pendingCombat.remaining - seconds); const currentSecond = Math.ceil(run.pendingCombat.remaining); if (currentSecond !== lastPendingCombatSecond) { lastPendingCombatSecond = currentSecond; save(false); } } if (run.pendingCombat.remaining <= 0 && !document.querySelector('dialog[open]')) startPendingCombat(); } function applyCombatEnemyPressure(seconds) { if (!run.combat.active || run.combat.target === 'watchdog' || seconds <= 0 || $('combatTutorialDialog').open || !state.meta.combatTutorialSeen) return; combatPressureCarry += seconds * Math.max(0.75, effectiveCombatStrength()); const interval = combatProfile()?.enemyAttackInterval || COMBAT_RULES.enemyAttackInterval; if (combatPressureCarry < interval) return; combatPressureCarry = Math.max(0, combatPressureCarry - interval); if (run.shields.bytes > 0) { run.shields.bytes--; combatFeedback = `GEGNERANGRIFF // 1 BYTE-SCHILD VERLOREN`; showCombatImpact('enemy-shield-hit', 'BYTE-SCHILD // -1'); tone(170, 0.09); return; } if (run.shields.bits > 0) { run.shields.bits--; combatFeedback = `GEGNERANGRIFF // 1 BIT-SCHILD VERLOREN`; showCombatImpact('enemy-shield-hit', 'BIT-SCHILD // -1'); tone(145, 0.08); return; } const stabilityDamage = (combatProfile()?.enemyStabilityDamage || COMBAT_RULES.enemyStabilityDamage) + combatBossPhase().damageBonus; run.stability = Math.max(0, run.stability - stabilityDamage); combatFeedback = `DIREKTANGRIFF // STABILITÄT ${Math.round(run.stability)}%`; showCombatImpact('enemy-direct-hit', `STABILITÄT // -${stabilityDamage}`); tone(72, 0.16); if (run.stability <= 0) { showStructuralCollapse(`${run.combat.targetName || 'Der Gegner'} hat deine Stabilität auf 0 gedrückt. Die Struktur kollabiert.`); } } function applyCombatRegeneration(seconds) { if (!run.combat.active || seconds <= 0 || $('combatTutorialDialog').open || !state.meta.combatTutorialSeen) return; const profile = combatProfile(); if (!profile?.repairRate || run.combat.health <= 0 || run.combat.health >= run.combat.maxHealth) return; if (run.combat.repairCooldown > 0) { run.combat.repairCooldown = Math.max(0, run.combat.repairCooldown - seconds); return; } const before = run.combat.health; run.combat.health = Math.min(run.combat.maxHealth, run.combat.health + profile.repairRate * seconds); if (Math.floor(run.combat.health) > Math.floor(before)) { combatFeedback = `SELBSTREPARATUR // INTEGRITÄT ${Math.ceil(run.combat.health)} / ${run.combat.maxHealth}`; } } function triggerScanner() { if (run.scannerTriggered || run.combat.active || run.pendingCombat.target || 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); showGuardedDialog($('eventDialog')); tone(90, 0.35); } function combatNeedlePosition() { const speed = COMBAT_RULES.baseNeedleSpeed + effectiveCombatStrength() * COMBAT_RULES.strengthSpeedBonus; const phase = (run.elapsed * speed) % 2; return phase <= 1 ? phase * 100 : (2 - phase) * 100; } function moveCombatZone() { const profile = combatProfile(); const bossPhase = combatBossPhase(); const baseWidth = profile?.zoneBase || COMBAT_RULES.baseZoneWidth; const penalty = profile?.zonePenalty || COMBAT_RULES.strengthZonePenalty; const minimumWidth = profile?.minimumZoneWidth || COMBAT_RULES.minimumZoneWidth; run.combat.zoneWidth = evolutionBossCombat() && profile?.bossZoneWidths ? profile.bossZoneWidths[Math.max(0, bossPhase.number - 1)] : Math.max(minimumWidth, baseWidth - effectiveCombatStrength() * penalty); run.combat.zoneStart = 4 + Math.random() * Math.max(1, 92 - run.combat.zoneWidth); } function renderCombat() { if (!run.combat.active) { $('eventDialog').classList.remove('boss-combat-active'); $('combatPanel').classList.remove('boss-combat-panel'); $('combatBossBanner').classList.add('hidden'); return; } const profile = combatProfile(); const ammo = combatAmmoCost(); const ammoText = formatResourceCost(ammo); const directThreat = combatDirectThreatActive(); const healthRatio = run.combat.maxHealth ? run.combat.health / run.combat.maxHealth : 0; const bossFight = evolutionBossCombat(); const bossPhase = combatBossPhase(); $('eventDialog').classList.toggle('boss-combat-active', bossFight); $('combatPanel').classList.toggle('boss-combat-panel', bossFight); $('combatBossBanner').classList.toggle('hidden', !bossFight); if (bossFight) { $('combatBossRank').textContent = profile.rank || 'EVOLUTIONS-SPERRINSTANZ'; $('combatBossPhase').textContent = `PHASE ${bossPhase.number} / 3 // ${bossPhase.label}`; document.querySelectorAll('[data-boss-phase]').forEach(marker => { const number = Number(marker.dataset.bossPhase); marker.classList.toggle('reached', number <= bossPhase.number); marker.classList.toggle('active', number === bossPhase.number); }); } $('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 = advancedSecurityCombat() ? `GEGENWEHR // ${Math.max(1, Math.ceil(effectiveCombatStrength() * 5))} // ADAPTIVE HÜLLE` : bossFight ? `GEGENWEHR // ${Math.max(1, Math.ceil(effectiveCombatStrength() * 5))} // ${bossPhase.label}` : `GEGENWEHR // ${Math.max(1, Math.ceil(effectiveCombatStrength() * 5))}`; $('combatFeedback').textContent = combatFeedback; $('combatVisual').src = profile?.visual || visuals.scannerSoftware || 'assets/software/enemy-scanner-software.svg'; renderResourceCost($('combatAmmoText'), ammo); $('combatPressureText').textContent = combatPressureText(); const enemyAttackInterval = profile?.enemyAttackInterval || COMBAT_RULES.enemyAttackInterval; const enemyPressureRate = Math.max(0.75, effectiveCombatStrength()); const enemyCharge = run.combat.target === 'watchdog' ? 0 : Math.min(1, combatPressureCarry / enemyAttackInterval); $('combatEnemyChargeMeter').style.width = `${enemyCharge * 100}%`; $('combatEnemyChargeText').textContent = run.combat.target === 'watchdog' ? 'INAKTIV' : enemyCharge >= 0.82 ? 'ANGRIFF BEREIT' : `LÄDT // ${Math.ceil(Math.max(0, enemyAttackInterval - combatPressureCarry) / enemyPressureRate)} S`; const repairVisible = Boolean(profile?.repairRate); $('combatRepair').classList.toggle('hidden', !repairVisible); if (repairVisible) { const cooldown = Math.max(0, run.combat.repairCooldown || 0); const repairReady = cooldown <= 0; $('combatRepairText').textContent = repairReady ? `AKTIV // +${String(profile.repairRate).replace('.', ',')} / S` : `BLOCKIERT // ${cooldown.toFixed(1).replace('.', ',')} S`; $('combatRepairMeter').style.width = `${repairReady ? 100 : Math.max(0, (1 - cooldown / profile.repairDelay) * 100)}%`; } const byteShield = run.shields.bytes; const bitShield = run.shields.bits; $('combatByteShield').textContent = n(byteShield); $('combatBitShield').textContent = n(bitShield); $('combatStabilityShield').textContent = `${Math.round(run.stability)}%`; $('combatByteShieldMeter').style.width = `${Math.min(100, byteShield / SHIELD_RULES.byteMax * 100)}%`; $('combatBitShieldMeter').style.width = `${Math.min(100, bitShield / SHIELD_RULES.bitMax * 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); const damage = combatDamageForAmmo(mode === 'bytes' ? { impulses: profile.shockCost, bytes: 1 } : mode === 'bits' ? { impulses: profile.shockCost, bits: 1 } : { impulses: profile.shockCost }); const assessment = combatAmmoAssessment(mode, profile); button.querySelector('span').textContent = mode === 'impulses' ? `${profile.shockCost} Impulse // ${damage} Schaden // ${assessment}` : mode === 'bits' ? `1 freies Bit // ${damage} Schaden // ${assessment} // Reserve danach ${n(Math.max(0, freeResourceAmount('bits') - 1))}` : `1 freies Byte // ${damage} Schaden // ${assessment} // Reserve danach ${n(Math.max(0, freeResourceAmount('bytes') - 1))}`; }); $('combatDirectThreat').classList.toggle('hidden', !directThreat); const canPayAmmo = canPayFreeResourceCost(ammo); $('combatPulseButton').disabled = !canPayAmmo; $('combatPulseButton').querySelector('small').textContent = !canPayAmmo ? `BENÖTIGT // ${ammoText}` : `KOSTET ${ammoText} // Geladener Schild bleibt getrennt`; $('combatRetreatButton').textContent = run.combat.target === 'watchdog' ? 'RESET ZULASSEN // BYTE ZERFÄLLT' : advancedSecurityCombat() ? 'KAMPF ABBRECHEN // VERSTECKEN' : 'RÜCKZUG // SPÄTER ERNEUT'; } 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; const profile = combatProfile(); if (!state.meta.combatTutorialSeen && !$('combatTutorialDialog').open) { openCombatTutorial(run.combat.target); return; } scannerOpening = true; $('eventAlert').textContent = run.combat.target === 'watchdog' ? '⚠ LETZTES LEBENSSIGNAL' : advancedSecurityCombat() ? '⚠ ANOMALIE ERKANNT' : '⚠ EVOLUTIONSPRÜFUNG'; $('eventTitle').textContent = run.combat.target === 'watchdog' ? 'WATCHDOG-NOTKAMPF' : profile?.name || 'UNBEKANNTE ABWEHR'; $('securityChoicePanel').classList.add('hidden'); $('combatPanel').classList.remove('hidden'); if (!document.querySelector('dialog[open]')) showGuardedDialog($('eventDialog')); renderCombat(); } function startCombat(target = 'security') { const mandatorySecurityFight = target === 'security' && !run.securityCombatResolved && !run.upgrades.subroutine; const scannerEventFight = target === 'security' && run.securityCombatResolved && run.upgrades.subroutine && !run.scannerTriggered; const profile = scannerEventFight ? ADVANCED_SECURITY_TARGET : COMBAT_TARGETS[target]; 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); if (target === 'scale') run.scaleGuardianTriggered = true; 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, repairCooldown: profile.repairDelay || 0, result: null }; moveCombatZone(); combatFeedback = target === 'watchdog' ? 'NOTLADUNG BEREIT // Watchdog treffen oder Reset zulassen.' : target === 'kernel' ? 'LAUFZEIT-ARBITER AKTIV // freie Bytes durchdringen die Scheduler-Hülle.' : target === 'scale' ? 'SKALIERUNGS-SENTINEL // schwere Munition einsetzen // Treffer sperren Reparatur.' : scannerEventFight ? 'ADAPTIVE HÜLLE // Impulse gedämpft // Treffer sperren Reparatur.' : 'SIGNATUR-PRÜFUNG // Stromstoß nur im Trefferfenster senden.'; addLog(target === 'watchdog' ? `LETZTES LEBENSSIGNAL: ${profile.emergencyCharge} Impulse werden als Notladung gebündelt.` : target === 'kernel' ? 'LAUFZEIT-ARBITER: Die dritte Softwareschicht berührt den Scheduler. Seine Kontrollinstanz verweigert dir jede eigene Laufzeit.' : target === 'scale' ? 'SKALIERUNGS-SENTINEL: Deine Prozessroutinen wachsen zu schnell. Die letzte lokale Kontrollinstanz sperrt automatische Synthese und fordert einen Kampf um den neuen Maßstab.' : scannerEventFight ? 'SICHERHEITS-SCAN STUFE 2: Die Routine erkennt deine frühere Stromstoß-Signatur. Ihre adaptive Hülle dämpft Impulse; strukturierte Bit- und Byte-Ladungen bleiben wirksam.' : 'SIGNATUR-PRÜFER: Zwei Softwareschichten tragen dein Muster. Die Kontrollroutine verlangt den Beweis, dass du kein flüchtiger Datenfehler mehr bist.', 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) { run.securityCombatResolved = true; run.combat.active = false; run.combat.result = 'won'; grantImpulses(80); run.bits += 2; run.stealth = Math.max(0, run.stealth - 8); unlockAchievement('signatureBoss'); scannerOpening = false; if ($('eventDialog').open) $('eventDialog').close(); addLog('SIGNATUR-PRÜFER BESIEGT: Das System akzeptiert dein Muster als dauerhafte Softwarestruktur. Die Subroutine kann jetzt gebunden werden. Belohnung: 80 Impulse und 2 freie Bits.', 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') { requestImmediateCombat('security'); return; } completeScannerChoice(choice); } function fireCombatPulse() { const profile = combatProfile(); 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.repairCooldown = profile.repairDelay || 0; 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`; showCombatImpact(critical ? 'critical-hit' : 'player-hit', `${critical ? 'DIREKTTREFFER' : 'TREFFER'} // -${damage}`); 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`; showCombatImpact('player-miss', `RÜCKKOPPLUNG // -${COMBAT_RULES.feedbackStabilityLoss}`); addLog(`STROMSTOSS VERFEHLT: Rückkopplung erreicht Stabilität${installedComponentIds().length ? ' und angeschlossene Systemschichten' : ''}.`, true); tone(105, 0.13); if (combatDirectThreatActive()) { showStructuralCollapse(`DIREKTANGRIFF: Ohne geladenen Kampfschild 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 === 'security' && !run.upgrades.subroutine) { run.stealth = Math.max(0, run.stealth - 10); run.stability = Math.max(1, run.stability - 5); addLog('RÜCKZUG VOR DEM SIGNATUR-PRÜFER: Die Evolution bleibt gesperrt. Sammle neue Reserven und fordere ihn später erneut heraus.', true); scannerOpening = false; if ($('eventDialog').open) $('eventDialog').close(); upgradesSignature = ''; render(); save(false); return; } if (run.combat.target === 'kernel') { run.stealth = Math.max(0, run.stealth - 18); run.stability = Math.max(1, run.stability - 8); addLog('RÜCKZUG VOR DEM LAUFZEIT-ARBITER: Der Schedulerbereich bleibt gesperrt. Nach neuen Reserven kannst du die Prozessprüfung erneut starten.', true); scannerOpening = false; if ($('eventDialog').open) $('eventDialog').close(); upgradesSignature = ''; render(); save(false); 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('RÜCKZUG VOR DEM SKALIERUNGS-SENTINEL: Die Mega-Synthese bleibt blockiert. Der Boss kann nach dem Wiederaufbau deiner Reserven erneut gewählt werden.', 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('LAUFZEIT-ARBITER BESIEGT: Sein Scheduler-Fenster bleibt offen. Der Aufstieg zum Prozess kann jetzt abgeschlossen werden.', 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); unlockAchievement('scalingBoss'); scannerOpening = false; if ($('eventDialog').open) $('eventDialog').close(); addLog('SKALIERUNGS-SENTINEL BESIEGT: Die Sperre über der automatischen Synthese fällt. Der Aufstieg zum Programm kann jetzt abgeschlossen werden.', 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 || run.pendingCombat.target || 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); showGuardedDialog($('proposalDialog')); 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 thoughtVoice() { if (run.stage !== 'program') return null; const primary = programVoiceKey(); return ['cooperative', 'pragmatic', 'illegal', 'hybrid', 'neutral'].includes(primary) ? primary : 'hybrid'; } function thoughtSourceLabel() { const voice = thoughtVoice(); if (!voice) return thoughtContent?.labels?.[run.stage] || `INNERES SIGNAL // ${run.stage.toUpperCase()}`; const labels = { cooperative: 'KOOPERATIV', pragmatic: 'PRAGMATISCH', illegal: 'ILLEGAL', hybrid: 'HYBRID', neutral: 'NEUTRAL' }; return `EIGENE STIMME // ${labels[voice]}`; } function displayThought(nextThought) { const box = $('thoughtBox'); const voice = thoughtVoice(); const source = thoughtSourceLabel(); if ($('thoughtSource').textContent !== source) $('thoughtSource').textContent = source; box.className = `thought-box thought-${run.stage}${voice ? ` voice-${voice}` : ''}`; void box.offsetWidth; box.classList.add('thought-changing'); $('thought').textContent = nextThought; lastThoughtText = nextThought; } function thoughts() { const options = [...(thoughtContent?.stages?.[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.'); if (run.stage === 'fragment' && installedComponentIds().length >= 2 && !run.securityCombatResolved) options.push('Eine fremde Instanz prüft, ob mein Muster bleiben darf.'); if (run.stage === 'subroutine' && run.components.io && !run.kernelResolved) options.push('Der Scheduler kennt meinen Takt. Noch verweigert er mir eigene Zeit.'); if (run.stage === 'process' && installedComponentIds().length >= COMPONENT_IDS.length && !run.scaleGuardianResolved) options.push('Das lokale Netz ist vollständig. Seine letzte Sperre beobachtet mein Wachstum.'); if (run.stage === 'program') { const voice = thoughtVoice(); if (thoughtContent?.morality?.[voice]) options.push(...thoughtContent.morality[voice]); } const candidates = options.length > 1 ? options.filter(text => text !== lastThoughtText) : options; const nextThought = candidates[Math.floor(Math.random() * candidates.length)] || options[0] || '…'; displayThought(nextThought); } 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 = Math.min(PROGRAM_LIMITS.resources.cycles, 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.`; showGuardedDialog($('offlineDialog')); } } 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 = Math.min(PROGRAM_LIMITS.resources.cycles, run.cycles + effectiveCycleRate() * dt); updateAutoUpgrader(dt); capProgramResources(); ageHardware(dt); updateProgramCare(dt); updateByteTrial(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(); triggerProposal(); if (run.combat.active) { applyCombatEnemyPressure(dt); applyCombatRegeneration(dt); } if (run.combat.active) openCombatDialog(); triggerScanner(); updatePendingCombat(dt); 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; combatTutorialTarget = null; combatTutorialStartMode = null; heldActivationKeys.clear(); pointerActivationHeld = false; cancelPulseHold(); } 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: COMBAT_TARGETS.kernel.name, health: COMBAT_TARGETS.kernel.health, maxHealth: COMBAT_TARGETS.kernel.health, strength: COMBAT_TARGETS.kernel.strength, zoneStart: 48, zoneWidth: 16, shots: 0, hits: 0, repairCooldown: 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: COMBAT_TARGETS.scale.name, health: COMBAT_TARGETS.scale.health, maxHealth: COMBAT_TARGETS.scale.health, strength: COMBAT_TARGETS.scale.strength, zoneStart: 42, zoneWidth: 20, shots: 0, hits: 0, repairCooldown: COMBAT_TARGETS.scale.repairDelay, 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, repairCooldown: 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') installSecondComponent(); if (name === 'subroutine') reachSubroutine(); if (name === 'kernel') reachKernelReady(); if (name === 'process') reachProcess(); if (name === 'fourthComponent') installFourthComponent(); 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: ADVANCED_SECURITY_TARGET.name, health: ADVANCED_SECURITY_TARGET.health, maxHealth: ADVANCED_SECURITY_TARGET.health, strength: ADVANCED_SECURITY_TARGET.strength, zoneStart: 48, zoneWidth: 22, shots: 0, hits: 0, repairCooldown: ADVANCED_SECURITY_TARGET.repairDelay, 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; autoUpgradeCarry = 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, megabytes: run.megabytes || 0, autoupgrader: run.autoUpgrader.unlocked ? run.autoUpgrader.enabled ? 'aktiv' : 'pausiert' : `${run.kilobytes || 0}/${AUTO_UPGRADER_UNLOCK_KILOBYTES} KB`, rechenzyklen: Number(run.cycles.toFixed(2)), stabilität: Number(run.stability.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', kampfWarnung: run.pendingCombat.target ? { ziel: COMBAT_TARGETS[run.pendingCombat.target]?.name, sekunden: Math.ceil(run.pendingCombat.remaining), quelle: run.pendingCombat.source } : 'inaktiv', kampfschild: clone(run.shields), kampfTutorial: state.meta.combatTutorialSeen ? 'gesehen' : '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', 'megabytes', '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' || key === 'megabytes') run[key] = Math.floor(run[key]); } }); upgradesSignature = ''; render(); save(false); return debugStatus(); } function debugAddImpulses(amount = 1000) { const gained = finite(amount, 0, 0, PROGRAM_LIMITS.resources.impulses); if (gained <= 0) { console.warn('AIWAKE: addImpulses erwartet eine positive Zahl.'); return debugStatus(); } pushDebugSnapshot(`vor AIWAKE.addImpulses(${gained})`); grantImpulses(gained); 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.restart()', Wirkung: 'Beginnt ein neues Spiel und legt vorher einen Rücksprung-Snapshot an. Manuelle Slots bleiben erhalten.' }, { Befehl: "AIWAKE.saveSlot('Test vor Boss')", Wirkung: 'Speichert den aktuellen Stand als benannten manuellen Slot.' }, { Befehl: "AIWAKE.loadSlot('Test vor Boss')", Wirkung: 'Lädt einen benannten manuellen Slot und sichert vorher den aktuellen Stand.' }, { Befehl: "AIWAKE.deleteSlot('Test vor Boss')", Wirkung: 'Löscht einen manuellen Slot.' }, { Befehl: 'AIWAKE.slots()', Wirkung: 'Listet alle manuellen Speicherstände.' }, { 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.addImpulses(1000)', Wirkung: 'Fügt dem aktuellen Stand 1.000 Impulse hinzu und legt vorher einen Snapshot an.' }, { 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, restart: restartGame, saveSlot: saveManualSlot, loadSlot: loadManualSlot, deleteSlot: deleteManualSlot, slots: () => readManualSaveSlots().map(manualSlotSummary), goto: debugGoto, next: debugNext, previous: debugPrevious, back: debugBack, status: debugStatus, addImpulses: debugAddImpulses, 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', () => { pointerActivationHeld = true; resumeAfterEvolution(); }, { capture: true }); document.addEventListener('pointermove', movePulseHold, { capture: true }); document.addEventListener('pointerup', event => { pointerActivationHeld = false; finishPulseHold(event); refreshDialogInputGuard(); }, { capture: true }); document.addEventListener('pointercancel', () => { pointerActivationHeld = false; cancelPulseHold(); refreshDialogInputGuard(); }, { capture: true }); document.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { heldActivationKeys.add(event.key); if (event.repeat) { event.preventDefault(); event.stopImmediatePropagation(); return; } } if (!['Shift', 'Control', 'Alt', 'Meta', 'CapsLock'].includes(event.key)) resumeAfterEvolution(); }, { capture: true }); document.addEventListener('keyup', event => { heldActivationKeys.delete(event.key); refreshDialogInputGuard(); }, { capture: true }); window.addEventListener('blur', () => { heldActivationKeys.clear(); pointerActivationHeld = false; cancelPulseHold(); refreshDialogInputGuard(); }); document.addEventListener('click', event => { const guardedDialog = event.target.closest?.('dialog.input-guarded'); if (!guardedDialog || !dialogInputGuardActive()) return; event.preventDefault(); event.stopImmediatePropagation(); }, { capture: true }); $('pixelBeing').addEventListener('pointerdown', event => beginPulseHold(event, $('pixelBeing'))); $('pixelBeing').addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); }); $('pixelBeing').addEventListener('keydown', event => { if (run.stage === 'bit' && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); pulse(null, 'keyboard'); } }); $('coreProgramAvatar').addEventListener('pointerdown', event => beginPulseHold(event, $('coreProgramAvatar'))); $('coreProgramAvatar').addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); }); $('pulseTarget').addEventListener('pointerdown', event => beginPulseHold(event, $('pulseTarget'))); $('pulseTarget').addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); }); $('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))); $('stabilityRepairButton').addEventListener('click', repairStabilityWithImpulses); document.querySelectorAll('[data-shield-charge]').forEach(button => button.addEventListener('click', () => chargeCombatShield(button.dataset.shieldCharge))); $('combatPulseButton').addEventListener('click', fireCombatPulse); $('combatHelpButton').addEventListener('click', () => openCombatTutorial(run.combat.target || run.pendingCombat.target || 'security')); $('combatRetreatButton').addEventListener('click', retreatCombat); $('encounterExplainButton').addEventListener('click', () => openCombatTutorial(run.pendingCombat.target || 'security')); $('encounterStartButton').addEventListener('click', () => { if (!run.pendingCombat.target) return; run.pendingCombat.remaining = 0; startPendingCombat(); }); $('combatTutorialContinue').addEventListener('click', completeCombatTutorial); $('combatTutorialDialog').addEventListener('cancel', event => event.preventDefault()); 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)); $('autoUpgraderToggle').addEventListener('click', () => { if (!run.autoUpgrader.unlocked) return; run.autoUpgrader.enabled = !run.autoUpgrader.enabled; autoUpgradeCarry = 0; upgradesSignature = ''; addLog(`AUTOUPGRADER ${run.autoUpgrader.enabled ? 'AKTIVIERT' : 'PAUSIERT'}: Die wiederholbare Produktionsschicht ${run.autoUpgrader.enabled ? 'verwaltet den Ausbau wieder selbstständig.' : 'nimmt keine weiteren Käufe vor.'}`, true); render(); save(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()); $('saveMenuButton').addEventListener('click', openSaveMenu); $('saveMenuClose').addEventListener('click', closeSaveMenu); $('manualSaveButton').addEventListener('click', promptManualSaveSlot); $('restartGameButton').addEventListener('click', confirmRestartGame); $('saveMenuDialog').addEventListener('close', () => { lastFrame = performance.now(); render(); }); $('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); })();