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