Files
AIWAKE/game.js

4948 lines
251 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

(() => {
'use strict';
const SAVE_KEY = 'aiwakeSaveV1';
const LEGACY_SAVE_KEY = 'bitAwakePrototypeV1';
const DEBUG_HISTORY_KEY = 'aiwakeDebugHistoryV1';
const LEGACY_DEBUG_HISTORY_KEY = 'bitAwakeDebugHistoryV1';
const 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);
})();