4948 lines
251 KiB
JavaScript
4948 lines
251 KiB
JavaScript
(() => {
|
||
'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);
|
||
})();
|