diff --git a/config.jsonc b/config.jsonc index 2dc56e0e..6ca68ec0 100644 --- a/config.jsonc +++ b/config.jsonc @@ -391,6 +391,11 @@ ], "categories": [] }, + "Five Nights at Epstein's": { + "path": "five-nights-at-epsteins", + "aliases": [], + "categories": [] + }, "Flash Chess": { "path": "flash/?game=flash-chess", "aliases": [], diff --git a/games/five-nights-at-epsteins/assets/images/Cam1.png b/games/five-nights-at-epsteins/assets/images/Cam1.png new file mode 100644 index 00000000..b13d163e Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam1.png differ diff --git a/games/five-nights-at-epsteins/assets/images/Cam10.png b/games/five-nights-at-epsteins/assets/images/Cam10.png new file mode 100644 index 00000000..409dab28 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam10.png differ diff --git a/games/five-nights-at-epsteins/assets/images/Cam11.png b/games/five-nights-at-epsteins/assets/images/Cam11.png new file mode 100644 index 00000000..7f9fb5c0 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam11.png differ diff --git a/games/five-nights-at-epsteins/assets/images/Cam2.png b/games/five-nights-at-epsteins/assets/images/Cam2.png new file mode 100644 index 00000000..2cf59fa3 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam2.png differ diff --git a/games/five-nights-at-epsteins/assets/images/Cam3.png b/games/five-nights-at-epsteins/assets/images/Cam3.png new file mode 100644 index 00000000..6de24844 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam3.png differ diff --git a/games/five-nights-at-epsteins/assets/images/Cam4.png b/games/five-nights-at-epsteins/assets/images/Cam4.png new file mode 100644 index 00000000..bbbb0303 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam4.png differ diff --git a/games/five-nights-at-epsteins/assets/images/Cam5.png b/games/five-nights-at-epsteins/assets/images/Cam5.png new file mode 100644 index 00000000..b63bb164 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam5.png differ diff --git a/games/five-nights-at-epsteins/assets/images/Cam6.png b/games/five-nights-at-epsteins/assets/images/Cam6.png new file mode 100644 index 00000000..0b5e8c9b Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam6.png differ diff --git a/games/five-nights-at-epsteins/assets/images/Cam7.png b/games/five-nights-at-epsteins/assets/images/Cam7.png new file mode 100644 index 00000000..15ff79c3 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam7.png differ diff --git a/games/five-nights-at-epsteins/assets/images/Cam8.png b/games/five-nights-at-epsteins/assets/images/Cam8.png new file mode 100644 index 00000000..43f6d908 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam8.png differ diff --git a/games/five-nights-at-epsteins/assets/images/Cam9.png b/games/five-nights-at-epsteins/assets/images/Cam9.png new file mode 100644 index 00000000..9f63f8b1 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/Cam9.png differ diff --git a/games/five-nights-at-epsteins/assets/images/FNAE-Map-layout.png b/games/five-nights-at-epsteins/assets/images/FNAE-Map-layout.png new file mode 100644 index 00000000..b79c3892 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/FNAE-Map-layout.png differ diff --git a/games/five-nights-at-epsteins/assets/images/cutscene.png b/games/five-nights-at-epsteins/assets/images/cutscene.png new file mode 100644 index 00000000..840294b8 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/cutscene.png differ diff --git a/games/five-nights-at-epsteins/assets/images/enemyep1.png b/games/five-nights-at-epsteins/assets/images/enemyep1.png new file mode 100644 index 00000000..c87943c6 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/enemyep1.png differ diff --git a/games/five-nights-at-epsteins/assets/images/enemyep4.png b/games/five-nights-at-epsteins/assets/images/enemyep4.png new file mode 100644 index 00000000..a3485d34 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/enemyep4.png differ diff --git a/games/five-nights-at-epsteins/assets/images/ep1.png b/games/five-nights-at-epsteins/assets/images/ep1.png new file mode 100644 index 00000000..85883dc4 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/ep1.png differ diff --git a/games/five-nights-at-epsteins/assets/images/ep4.png b/games/five-nights-at-epsteins/assets/images/ep4.png new file mode 100644 index 00000000..ddce0dfb Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/ep4.png differ diff --git a/games/five-nights-at-epsteins/assets/images/fa3.png b/games/five-nights-at-epsteins/assets/images/fa3.png new file mode 100644 index 00000000..1a21a199 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/fa3.png differ diff --git a/games/five-nights-at-epsteins/assets/images/goldenstephen.png b/games/five-nights-at-epsteins/assets/images/goldenstephen.png new file mode 100644 index 00000000..bc4e4fa4 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/goldenstephen.png differ diff --git a/games/five-nights-at-epsteins/assets/images/jump.png b/games/five-nights-at-epsteins/assets/images/jump.png new file mode 100644 index 00000000..6462f3e6 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/jump.png differ diff --git a/games/five-nights-at-epsteins/assets/images/jumptrump.png b/games/five-nights-at-epsteins/assets/images/jumptrump.png new file mode 100644 index 00000000..77251281 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/jumptrump.png differ diff --git a/games/five-nights-at-epsteins/assets/images/menubackground.png b/games/five-nights-at-epsteins/assets/images/menubackground.png new file mode 100644 index 00000000..9289856e Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/menubackground.png differ diff --git a/games/five-nights-at-epsteins/assets/images/original.png b/games/five-nights-at-epsteins/assets/images/original.png new file mode 100644 index 00000000..42d7fd36 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/original.png differ diff --git a/games/five-nights-at-epsteins/assets/images/scaryep.png b/games/five-nights-at-epsteins/assets/images/scaryep.png new file mode 100644 index 00000000..3f8c79b4 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/scaryep.png differ diff --git a/games/five-nights-at-epsteins/assets/images/scaryhawk.png b/games/five-nights-at-epsteins/assets/images/scaryhawk.png new file mode 100644 index 00000000..35bb35a2 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/scaryhawk.png differ diff --git a/games/five-nights-at-epsteins/assets/images/scaryhawking.png b/games/five-nights-at-epsteins/assets/images/scaryhawking.png new file mode 100644 index 00000000..eb774cbd Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/scaryhawking.png differ diff --git a/games/five-nights-at-epsteins/assets/images/scarytrump.png b/games/five-nights-at-epsteins/assets/images/scarytrump.png new file mode 100644 index 00000000..11690856 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/scarytrump.png differ diff --git a/games/five-nights-at-epsteins/assets/images/star.png b/games/five-nights-at-epsteins/assets/images/star.png new file mode 100644 index 00000000..134ce2d3 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/star.png differ diff --git a/games/five-nights-at-epsteins/assets/images/winscreen.png b/games/five-nights-at-epsteins/assets/images/winscreen.png new file mode 100644 index 00000000..3fd88d1f Binary files /dev/null and b/games/five-nights-at-epsteins/assets/images/winscreen.png differ diff --git a/games/five-nights-at-epsteins/assets/sounds/1.ogg b/games/five-nights-at-epsteins/assets/sounds/1.ogg new file mode 100644 index 00000000..365ed27d Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/1.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/2.ogg b/games/five-nights-at-epsteins/assets/sounds/2.ogg new file mode 100644 index 00000000..e6b42729 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/2.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/Blip.ogg b/games/five-nights-at-epsteins/assets/sounds/Blip.ogg new file mode 100644 index 00000000..d15412a0 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/Blip.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/Crank1.ogg b/games/five-nights-at-epsteins/assets/sounds/Crank1.ogg new file mode 100644 index 00000000..8226aa99 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/Crank1.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/Crank2.ogg b/games/five-nights-at-epsteins/assets/sounds/Crank2.ogg new file mode 100644 index 00000000..98e7de24 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/Crank2.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/Static_sound.ogg b/games/five-nights-at-epsteins/assets/sounds/Static_sound.ogg new file mode 100644 index 00000000..4e314d8a Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/Static_sound.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/chimes.ogg b/games/five-nights-at-epsteins/assets/sounds/chimes.ogg new file mode 100644 index 00000000..0dafa375 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/chimes.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/ekg.wav b/games/five-nights-at-epsteins/assets/sounds/ekg.wav new file mode 100644 index 00000000..93f3e64d Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/ekg.wav differ diff --git a/games/five-nights-at-epsteins/assets/sounds/goldenstephenscare.ogg b/games/five-nights-at-epsteins/assets/sounds/goldenstephenscare.ogg new file mode 100644 index 00000000..85071a36 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/goldenstephenscare.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/hawking_shock.wav b/games/five-nights-at-epsteins/assets/sounds/hawking_shock.wav new file mode 100644 index 00000000..dcf2d9b7 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/hawking_shock.wav differ diff --git a/games/five-nights-at-epsteins/assets/sounds/jumpcare.ogg b/games/five-nights-at-epsteins/assets/sounds/jumpcare.ogg new file mode 100644 index 00000000..8d490ec8 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/jumpcare.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/music.ogg b/games/five-nights-at-epsteins/assets/sounds/music.ogg new file mode 100644 index 00000000..a5c18f23 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/music.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/music3.ogg b/games/five-nights-at-epsteins/assets/sounds/music3.ogg new file mode 100644 index 00000000..afeab609 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/music3.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/stephenjumpscare.ogg b/games/five-nights-at-epsteins/assets/sounds/stephenjumpscare.ogg new file mode 100644 index 00000000..0e2cc5c3 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/stephenjumpscare.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/vent-crawling.mp3 b/games/five-nights-at-epsteins/assets/sounds/vent-crawling.mp3 new file mode 100644 index 00000000..2c4c20ff Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/vent-crawling.mp3 differ diff --git a/games/five-nights-at-epsteins/assets/sounds/vents.ogg b/games/five-nights-at-epsteins/assets/sounds/vents.ogg new file mode 100644 index 00000000..3214eea4 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/vents.ogg differ diff --git a/games/five-nights-at-epsteins/assets/sounds/winmusic.ogg b/games/five-nights-at-epsteins/assets/sounds/winmusic.ogg new file mode 100644 index 00000000..ea175e72 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/sounds/winmusic.ogg differ diff --git a/games/five-nights-at-epsteins/assets/vedio/202512191935.webm b/games/five-nights-at-epsteins/assets/vedio/202512191935.webm new file mode 100644 index 00000000..a084d954 Binary files /dev/null and b/games/five-nights-at-epsteins/assets/vedio/202512191935.webm differ diff --git a/games/five-nights-at-epsteins/favicon.ico b/games/five-nights-at-epsteins/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/games/five-nights-at-epsteins/favicon.ico differ diff --git a/games/five-nights-at-epsteins/index.html b/games/five-nights-at-epsteins/index.html new file mode 100644 index 00000000..7908bfb1 --- /dev/null +++ b/games/five-nights-at-epsteins/index.html @@ -0,0 +1,246 @@ + + + + + + + Five Nights At Epstein's - Web Version + + + + + + +
+
+ +
+
+
+
+
+
LOADING...
+
+
+
+
0%
+
+
+ +
+ +
+ + + +
+ + +
+ +
+
12 AM
+
NIGHT 1
+
+ + +
+
+ Vent + 100%O2 +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/games/five-nights-at-epsteins/js/AssetManager.js b/games/five-nights-at-epsteins/js/AssetManager.js new file mode 100644 index 00000000..71f255c8 --- /dev/null +++ b/games/five-nights-at-epsteins/js/AssetManager.js @@ -0,0 +1,169 @@ +// 资源管理器 +class AssetManager { + constructor() { + this.images = {}; + this.sounds = {}; + this.loaded = false; + + // 分类音量设置 + this.volumeSettings = this.loadVolumeSettings(); + } + + // 从 localStorage 加载音量设置 + loadVolumeSettings() { + const saved = localStorage.getItem('fnae_volume_settings'); + if (saved) { + return JSON.parse(saved); + } + // 默认音量设置 + return { + master: 0.7, + gameBg: 0.7, + menuMusic: 0.7, + jumpscare: 0.7, + ventCrawling: 0.7 + }; + } + + // 保存音量设置 + saveVolumeSettings() { + localStorage.setItem('fnae_volume_settings', JSON.stringify(this.volumeSettings)); + } + + // 设置特定类型的音量 + setVolume(type, volume) { + this.volumeSettings[type] = Math.max(0, Math.min(1, volume)); + this.saveVolumeSettings(); + } + + // 获取特定类型的音量 + getVolume(type) { + return this.volumeSettings[type] || 0.7; + } + + // 获取所有音量设置 + getAllVolumes() { + return this.volumeSettings; + } + + async loadAssets() { + // 获取当前脚本的基础路径 + const basePath = './'; + + // 从 Unity 提取的资源 + const imagePaths = { + office: `${basePath}assets/images/original.png`, + cam1: `${basePath}assets/images/Cam1.png`, + cam2: `${basePath}assets/images/Cam2.png`, + cam3: `${basePath}assets/images/Cam3.png`, + cam4: `${basePath}assets/images/Cam4.png`, + cam5: `${basePath}assets/images/Cam5.png`, + cam6: `${basePath}assets/images/Cam6.png`, + cam7: `${basePath}assets/images/Cam7.png`, + cam8: `${basePath}assets/images/Cam8.png`, + cam9: `${basePath}assets/images/Cam9.png`, + cam10: `${basePath}assets/images/Cam10.png`, + cam11: `${basePath}assets/images/Cam11.png`, + jumpscare: `${basePath}assets/images/jump.png`, // EP跳杀图片 + trumpJumpscare: `${basePath}assets/images/jumptrump.png`, // Trump跳杀图片 + hawkingJumpscare: `${basePath}assets/images/scaryhawking.png`, // Hawking跳杀图片 + }; + + const soundPaths = { + ambient: `${basePath}assets/sounds/music.ogg`, + static: `${basePath}assets/sounds/Static_sound.ogg`, + staticLoop: `${basePath}assets/sounds/Static_sound.ogg`, + vents: `${basePath}assets/sounds/vents.ogg`, + ventCrawling: `${basePath}assets/sounds/vent-crawling.mp3`, + jumpscare: `${basePath}assets/sounds/jumpcare.ogg`, + hawkingJumpscare: `${basePath}assets/sounds/stephenjumpscare.ogg`, // Hawking跳杀音效 + blip: `${basePath}assets/sounds/Blip.ogg`, + win: `${basePath}assets/sounds/winmusic.ogg`, + chimes: `${basePath}assets/sounds/chimes.ogg`, + crank1: `${basePath}assets/sounds/Crank1.ogg`, + crank2: `${basePath}assets/sounds/Crank2.ogg`, + ekg: `${basePath}assets/sounds/ekg.wav`, + hawking_shock: `${basePath}assets/sounds/hawking_shock.wav`, + goldenstephenscare: `${basePath}assets/sounds/goldenstephenscare.ogg`, // Golden 霍金音效 + }; + + // 加载图片 + for (const [key, path] of Object.entries(imagePaths)) { + try { + this.images[key] = await this.loadImage(path); + } catch (e) { + console.warn(`Failed to load image: ${path}`); + } + } + + // 加载音频 + for (const [key, path] of Object.entries(soundPaths)) { + try { + this.sounds[key] = new Audio(path); + } catch (e) { + console.warn(`Failed to load sound: ${path}`); + } + } + + this.loaded = true; + } + + loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); + } + + playSound(key, loop = false, volume = 1.0) { + if (this.sounds[key]) { + this.sounds[key].loop = loop; + + // 根据音效类型应用对应的音量 + let categoryVolume = this.volumeSettings.master; + + if (key === 'music' || key === 'music3') { + categoryVolume *= this.volumeSettings.menuMusic; + } else if (key === 'jumpscare' || key === 'hawkingJumpscare' || key === 'trumpJumpscare') { + categoryVolume *= this.volumeSettings.jumpscare; + } else if (key === 'ventCrawling') { + categoryVolume *= this.volumeSettings.ventCrawling; + } else if (key === 'vents' || key === 'ambience' || key === 'staticLoop' || key === 'static' || key === 'blip' || key === 'Blip') { + // 游戏背景音乐:包括通风口声音、静态噪声、摄像机切换声等 + categoryVolume *= this.volumeSettings.gameBg; + } + + this.sounds[key].volume = Math.min(1, volume * categoryVolume); + this.sounds[key].play(); + } + } + + stopSound(key) { + if (this.sounds[key]) { + this.sounds[key].pause(); + this.sounds[key].currentTime = 0; + } + } + + setSoundVolume(key, volume) { + if (this.sounds[key]) { + // 根据音效类型应用对应的音量 + let categoryVolume = this.volumeSettings.master; + + if (key === 'music' || key === 'music3') { + categoryVolume *= this.volumeSettings.menuMusic; + } else if (key === 'jumpscare' || key === 'hawkingJumpscare' || key === 'trumpJumpscare') { + categoryVolume *= this.volumeSettings.jumpscare; + } else if (key === 'ventCrawling') { + categoryVolume *= this.volumeSettings.ventCrawling; + } else if (key === 'vents' || key === 'ambience' || key === 'staticLoop' || key === 'static' || key === 'blip' || key === 'Blip') { + // 游戏背景音乐:包括通风口声音、静态噪声、摄像机切换声等 + categoryVolume *= this.volumeSettings.gameBg; + } + + this.sounds[key].volume = Math.min(1, volume * categoryVolume); + } + } +} diff --git a/games/five-nights-at-epsteins/js/CameraSystem.js b/games/five-nights-at-epsteins/js/CameraSystem.js new file mode 100644 index 00000000..fd77c644 --- /dev/null +++ b/games/five-nights-at-epsteins/js/CameraSystem.js @@ -0,0 +1,1045 @@ +// Camera system management +class CameraSystem { + constructor(game) { + this.game = game; + this.cameraPanel = document.getElementById('camera-panel'); + this.currentCamLabel = document.getElementById('current-cam-label'); + this.cameraErrorLabel = document.getElementById('camera-error-label'); + this.playSoundBtn = document.getElementById('play-sound-btn'); + this.shockHawkingBtn = document.getElementById('shock-hawking-btn'); + this.currentSoundToggle = false; + this.staticVideo = document.getElementById('camera-static-video'); + + // 播放声音按钮状态 + this.soundButtonCooldown = false; + this.soundButtonUseCount = 0; + this.maxSoundUses = 5; // 连续使用5次后摄像头故障 + this.cooldownTime = 8000; // 8秒冷却 + this.cooldownInterval = null; // 冷却动画定时器 + + // 每个位置的连续吸引计数 + this.locationAttractCount = {}; // { 'cam11': 2, 'cam8': 1, ... } + this.maxLocationAttractCount = 2; // 同一位置最多连续吸引2次 + this.lastEpLocation = null; // 记录EP的上一个位置,用于检测移动 + + // EP 角色配置 - 直接引用 EnemyAI 的配置(游戏初始化后会设置) + this.characterImages = null; + this.characterPositions = null; + this.characterBrightness = null; + this.characterRotation = null; + + this.bindEvents(); + } + + // Initialize EP config (from EnemyAI) + initEPConfig() { + if (this.game.enemyAI) { + this.characterImages = this.game.enemyAI.characterImages; + this.characterPositions = this.game.enemyAI.characterPositions; + this.characterBrightness = this.game.enemyAI.characterBrightness; + this.characterRotation = this.game.enemyAI.characterRotation; + console.log('EP config initialized from EnemyAI'); + } + } + + bindEvents() { + if (this.playSoundBtn) { + this.playSoundBtn.addEventListener('click', () => this.playAmbientSound()); + } + if (this.shockHawkingBtn) { + this.shockHawkingBtn.addEventListener('click', () => this.shockHawking()); + } + } + + toggle() { + // console.log('📷 Camera toggle called, current state:', this.game.state.cameraOpen); + if (this.game.state.cameraOpen) { + this.close(); + } else { + this.open(); + } + } + + open() { + // console.log('📷 Opening camera...'); + // console.log('📷 Camera panel element:', this.cameraPanel); + // console.log('📷 Camera panel classes before:', this.cameraPanel.className); + + this.game.state.cameraOpen = true; + this.cameraPanel.classList.remove('hidden'); + this.cameraPanel.classList.add('show'); + + // console.log('📷 Camera panel classes after:', this.cameraPanel.className); + // console.log('📷 Camera panel display:', window.getComputedStyle(this.cameraPanel).display); + // console.log('📷 Camera panel opacity:', window.getComputedStyle(this.cameraPanel).opacity); + // console.log('📷 Camera panel transform:', window.getComputedStyle(this.cameraPanel).transform); + + this.game.assets.playSound('crank1'); + + // Start looping low volume static sound + this.game.assets.playSound('staticLoop', true, 0.3); + + this.createCameraGrid(); + + // 更新电击按钮显示 + this.updateShockButtonVisibility(); + + // 更新霍金警告位置(从风扇左边移到地图上) + if (this.game.enemyAI && this.game.enemyAI.hawking.active) { + this.game.enemyAI.updateHawkingWarningDisplay(); + } + + // If camera failed, show failure effect + if (this.game.state.cameraFailed) { + console.log('📷 Camera is failed, showing failure effect'); + this.showCameraFailure(); + } else { + console.log('📷 Camera is normal, showing normal view'); + // Normal state, ensure all failure effects removed + this.cameraPanel.classList.remove('transitioning'); + + // Hide ERR label + if (this.cameraErrorLabel) { + this.cameraErrorLabel.classList.remove('active'); + } + + // Stop static + this.stopStatic(); + + // Show map + const cameraGrid = document.getElementById('camera-grid'); + if (cameraGrid) { + cameraGrid.style.display = 'block'; + } + + // Update view + this.updateView(); + } + + // Stop view rotation + this.game.isRotatingLeft = false; + this.game.isRotatingRight = false; + } + + // Show camera failure effect + showCameraFailure() { + console.log('Showing camera failure effect...'); + + // Night 5: 30% 概率触发 Golden 霍金彩蛋 + if (this.game.state.currentNight === 5 && Math.random() < 0.3) { + this.game.showGoldenStephen(); + } + + // Hide background image and characters + this.cameraPanel.classList.add('transitioning'); + + // Hide map + const cameraGrid = document.getElementById('camera-grid'); + if (cameraGrid) { + cameraGrid.style.display = 'none'; + console.log('Camera grid hidden'); + } + + // Show ERR label + if (this.cameraErrorLabel) { + this.cameraErrorLabel.classList.add('active'); + console.log('ERR label shown'); + } + + // Show and play static video + if (this.staticVideo) { + console.log('Starting static video...'); + this.staticVideo.classList.add('active'); + this.staticVideo.currentTime = 0; // Play from beginning + this.staticVideo.play().catch(e => console.log('Video playback failed:', e)); + } else { + console.error('Static video element not found!'); + } + } + + // Stop static effect + stopStatic() { + if (this.staticVideo) { + this.staticVideo.classList.remove('active'); + this.staticVideo.pause(); + this.staticVideo.currentTime = 0; + } + } + + // Start static effect (for switching cameras) + startStatic() { + if (this.staticVideo) { + this.staticVideo.classList.add('active'); + this.staticVideo.play().catch(e => console.log('Video playback failed:', e)); + } + } + + // Restore camera normal display + restoreCameraView() { + console.log('Restoring camera view...'); + + // Stop static + this.stopStatic(); + console.log('Static video stopped'); + + // Remove failure state + this.cameraPanel.classList.remove('transitioning'); + console.log('Removed transitioning class'); + + // Hide ERR label + if (this.cameraErrorLabel) { + this.cameraErrorLabel.classList.remove('active'); + console.log('ERR label hidden'); + } + + // Show map + const cameraGrid = document.getElementById('camera-grid'); + if (cameraGrid) { + cameraGrid.style.display = 'block'; + console.log('Camera grid shown'); + } + + // Update view + this.updateView(); + console.log('View updated'); + } + + // Fix camera + restartCamera() { + // 如果控制面板正忙,不允许操作 + if (this.game.state.controlPanelBusy) { + console.log('Control panel is busy, cannot restart camera'); + return; + } + + console.log('Restarting camera system...'); + this.game.state.cameraRestarting = true; + this.game.state.controlPanelBusy = true; // 锁定控制面板 + + // 播放心电图音效 + this.game.assets.playSound('ekg', false, 0.8); + + // Restore after 4 seconds + setTimeout(() => { + // 无论之前是否故障,重启后都恢复正常 + this.game.state.cameraFailed = false; + this.game.state.cameraRestarting = false; + this.game.state.controlPanelBusy = false; // 解锁控制面板 + + // Stop static noise (如果有的话) + this.game.assets.stopSound('static'); + + // Reset sound button count (恢复5次使用次数) + this.resetSoundButtonCount(); + + console.log('Camera system restored!'); + + // If camera is open, immediately restore display + if (this.game.state.cameraOpen) { + console.log('Camera is open, restoring view...'); + this.restoreCameraView(); + } + }, 4000); + } + + close() { + this.game.state.cameraOpen = false; + this.cameraPanel.classList.add('closing'); + this.cameraPanel.classList.remove('show'); + + // Stop looping static sound + this.game.assets.stopSound('staticLoop'); + + // Clear character display + const characterOverlay = document.getElementById('character-overlay'); + if (characterOverlay) { + characterOverlay.innerHTML = ''; + console.log('Character overlay cleared'); + } + + // 更新霍金警告位置(从地图移到风扇左边) + if (this.game.enemyAI && this.game.enemyAI.hawking.active) { + this.game.enemyAI.updateHawkingWarningDisplay(); + } + + setTimeout(() => { + this.cameraPanel.classList.add('hidden'); + this.cameraPanel.classList.remove('closing'); + }, 400); + + this.game.assets.playSound('crank2'); + } + + switchCamera(camNum) { + // If camera failed, cannot switch + if (this.game.state.cameraFailed) { + console.log('Camera system is offline! Cannot switch cameras.'); + return; + } + + // Add transition state, hide background image + this.cameraPanel.classList.add('transitioning'); + + // Hide map + const cameraGrid = document.getElementById('camera-grid'); + if (cameraGrid) { + cameraGrid.style.display = 'none'; + } + + // 隐藏角色 + const characterOverlay = document.getElementById('character-overlay'); + if (characterOverlay) { + characterOverlay.style.display = 'none'; + } + + // 暂时降低循环静态音的音量 + this.game.assets.setSoundVolume('staticLoop', 0.1); + + // 播放正常音量的静态音效 + this.game.assets.playSound('static', false, 1.0); + + // 1000ms 后停止静态音效 + setTimeout(() => { + this.game.assets.stopSound('static'); + }, 1000); + + // Show static effect + this.startStatic(); + + // Switch camera after 500ms + setTimeout(() => { + // If camera already failed, stop switch animation, show failure effect + if (this.game.state.cameraFailed) { + console.log('Camera failed during switch, showing failure effect'); + this.showCameraFailure(); + return; + } + + this.game.state.currentCam = `cam${camNum}`; + this.updateView(); + this.createCameraGrid(); + + // After another 500ms fade out static, restore background + setTimeout(() => { + // Check again if failed + if (this.game.state.cameraFailed) { + console.log('Camera failed during switch, showing failure effect'); + this.showCameraFailure(); + return; + } + + this.stopStatic(); + this.cameraPanel.classList.remove('transitioning'); + + // 显示地图 + if (cameraGrid) { + cameraGrid.style.display = 'block'; + } + + // 显示角色 + if (characterOverlay) { + characterOverlay.style.display = 'block'; + } + + // 更新电击按钮显示(根据当前摄像头) + this.updateShockButtonVisibility(); + + // 恢复循环静态音的音量 + this.game.assets.setSoundVolume('staticLoop', 0.3); + }, 500); + }, 500); + } + + updateView() { + // If camera failed, don't update view + if (this.game.state.cameraFailed) { + return; + } + + // Update camera panel background image + if (this.game.assets.images[this.game.state.currentCam]) { + this.cameraPanel.style.backgroundImage = `url('${this.game.assets.images[this.game.state.currentCam].src}')`; + } + + // 更新摄像头标签 + const camNum = this.game.state.currentCam.replace('cam', ''); + this.currentCamLabel.textContent = `CAM ${camNum}`; + + // 更新角色显示 + this.updateCharacterDisplay(); + + // 更新电击按钮显示 + this.updateShockButtonVisibility(); + } + + // 更新角色显示(支持多个敌人) + updateCharacterDisplay() { + const currentCam = this.game.state.currentCam; + const epLocation = this.game.enemyAI.getCurrentLocation(); + const trumpLocation = this.game.enemyAI.getTrumpCurrentLocation(); + const hawkingActive = this.game.enemyAI.hawking.active; + + console.log(`updateCharacterDisplay - Current Cam: ${currentCam}, EP: ${epLocation}, Trump: ${trumpLocation}, Hawking: ${hawkingActive}, Night: ${this.game.state.currentNight}`); + + // 打印所有相关元素的z-index + console.log('🔍 Z-Index Debug:'); + console.log(' - cameraPanel:', window.getComputedStyle(this.cameraPanel).zIndex); + const staticVideo = document.getElementById('camera-static-video'); + if (staticVideo) { + console.log(' - staticVideo:', window.getComputedStyle(staticVideo).zIndex); + } + const existingOverlay = document.getElementById('character-overlay'); + if (existingOverlay) { + console.log(' - characterOverlay:', window.getComputedStyle(existingOverlay).zIndex); + console.log(' - characterOverlay display:', window.getComputedStyle(existingOverlay).display); + console.log(' - characterOverlay children count:', existingOverlay.children.length); + } + + // 获取或创建角色容器 + let characterOverlay = document.getElementById('character-overlay'); + if (!characterOverlay) { + characterOverlay = document.createElement('div'); + characterOverlay.id = 'character-overlay'; + characterOverlay.style.position = 'absolute'; + characterOverlay.style.top = '0'; + characterOverlay.style.left = '0'; + characterOverlay.style.width = '100%'; + characterOverlay.style.height = '100%'; + characterOverlay.style.pointerEvents = 'none'; + characterOverlay.style.zIndex = '5'; + characterOverlay.style.overflow = 'hidden'; + this.cameraPanel.appendChild(characterOverlay); + } + + // 清空之前的角色 + characterOverlay.innerHTML = ''; + + console.log('🔍 Character overlay cleared, checking EP display conditions...'); + console.log('🔍 EP hasSpawned:', this.game.enemyAI.epstein.hasSpawned); + console.log('🔍 EP location matches current cam:', epLocation === currentCam); + console.log('🔍 Has characterImages:', !!this.characterImages); + console.log('🔍 Has image for current cam:', this.characterImages ? !!this.characterImages[currentCam] : 'N/A'); + + // 显示霍金(如果激活且在cam6) + if (hawkingActive && currentCam === 'cam6') { + const hawkingImg = document.createElement('img'); + hawkingImg.src = '/assets/images/mrstephen.png'; + hawkingImg.style.position = 'absolute'; + hawkingImg.className = 'visible hawking-character'; + hawkingImg.style.zIndex = '3'; // Hawking 在最上层 + hawkingImg.style.left = '59.6%'; + hawkingImg.style.bottom = '0.9%'; + hawkingImg.style.width = '37%'; + hawkingImg.style.transform = 'translateX(-50%) rotate(-5deg)'; + hawkingImg.style.filter = 'brightness(0.33) contrast(1) saturate(1)'; + + characterOverlay.appendChild(hawkingImg); + console.log(`✓ Displaying Hawking at cam6`); + } + + // 显示 EP(如果已出场且在当前摄像头) + // console.log('🔍 EP Display Check:', { + // hasSpawned: this.game.enemyAI.epstein.hasSpawned, + // epLocation: epLocation, + // currentCam: currentCam, + // match: epLocation === currentCam, + // hasImage: !!this.characterImages, + // imageForCam: this.characterImages ? !!this.characterImages[currentCam] : 'N/A' + // }); + + if (this.game.enemyAI.epstein.hasSpawned && epLocation === currentCam && this.characterImages && this.characterImages[currentCam]) { + // 创建EP容器(用于包含EP图片和电眼) + const epContainer = document.createElement('div'); + epContainer.className = 'ep-container'; + epContainer.style.position = 'absolute'; + epContainer.style.zIndex = '1'; + + const pos = this.characterPositions[currentCam]; + if (pos) { + if (pos.left) { + epContainer.style.left = pos.left; + epContainer.style.right = 'auto'; + } else if (pos.right) { + epContainer.style.right = pos.right; + epContainer.style.left = 'auto'; + } + + epContainer.style.bottom = pos.bottom; + epContainer.style.width = pos.width; + epContainer.style.transform = pos.transform || 'none'; + } + + // EP图片 + const epImg = document.createElement('img'); + epImg.src = this.characterImages[currentCam]; + epImg.style.position = 'relative'; + epImg.style.width = '100%'; + epImg.style.height = 'auto'; + epImg.style.display = 'block'; + epImg.className = 'visible ep-character'; + + // 应用明暗度 + const brightness = this.characterBrightness[currentCam] || 100; + epImg.style.filter = `brightness(${brightness}%)`; + + epContainer.appendChild(epImg); + characterOverlay.appendChild(epContainer); + console.log(`✓ Displaying EP at ${currentCam}`); + + // Night 6: 渲染电眼特效(作为EP容器的子元素) + if (this.game.state.currentNight === 6) { + this.renderLightningEyes(epContainer, currentCam); + } + } + + // 显示 Trump(如果已出场且在当前摄像头,且不在爬行状态,且当前夜晚有Trump配置) + if (this.game.enemyAI.trump.hasSpawned && !this.game.enemyAI.trump.isCrawling && trumpLocation === currentCam && this.game.enemyAI.currentTrumpConfig) { + const trumpImages = this.game.enemyAI.trumpImages; + const trumpPositions = this.game.enemyAI.trumpPositions; + const trumpBrightness = this.game.enemyAI.trumpBrightness; + + if (trumpImages[currentCam]) { + const trumpImg = document.createElement('img'); + trumpImg.src = trumpImages[currentCam]; + trumpImg.style.position = 'absolute'; + trumpImg.className = 'visible trump-character'; + trumpImg.style.zIndex = '2'; // Trump 在上层 + + const pos = trumpPositions[currentCam]; + if (pos) { + if (pos.left) { + trumpImg.style.left = pos.left; + trumpImg.style.right = 'auto'; + } else if (pos.right) { + trumpImg.style.right = pos.right; + trumpImg.style.left = 'auto'; + } + + trumpImg.style.bottom = pos.bottom; + trumpImg.style.width = pos.width; + trumpImg.style.transform = pos.transform || 'none'; + } + + const brightness = trumpBrightness[currentCam] || 100; + trumpImg.style.filter = `brightness(${brightness}%)`; + + characterOverlay.appendChild(trumpImg); + console.log(`✓ Displaying Trump at ${currentCam}`); + } + } + + if (characterOverlay.children.length === 0) { + console.log(`✗ No characters at current camera (viewing ${currentCam})`); + } + } + + createCameraGrid() { + const grid = document.getElementById('camera-grid'); + grid.innerHTML = ''; + + // 创建地图容器 + const mapContainer = document.createElement('div'); + mapContainer.style.position = 'relative'; + mapContainer.style.width = '100%'; + mapContainer.style.height = '100%'; + + // 添加地图图片 + const mapImg = document.createElement('img'); + mapImg.src = '/assets/images/FNAE-Map-layout.png'; + mapImg.style.width = '100%'; + mapImg.style.height = 'auto'; + mapImg.style.display = 'block'; + mapContainer.appendChild(mapImg); + + // 添加 YOU 标记(玩家位置) + const youMarker = document.createElement('div'); + youMarker.style.position = 'absolute'; + youMarker.style.left = '7.0%'; + youMarker.style.top = '82.6%'; + youMarker.style.width = '13.0%'; + youMarker.style.height = '8.0%'; + youMarker.style.display = 'flex'; + youMarker.style.alignItems = 'center'; + youMarker.style.justifyContent = 'center'; + youMarker.style.fontSize = '0.7vw'; + youMarker.style.fontWeight = 'bold'; + youMarker.style.color = '#fff'; + youMarker.style.textShadow = '1px 1px 2px #000'; + youMarker.style.fontFamily = 'Arial, sans-serif'; + youMarker.style.background = 'rgba(0, 0, 0, 0.5)'; + youMarker.style.borderRadius = '4px'; + youMarker.textContent = 'YOU'; + mapContainer.appendChild(youMarker); + + // 定义每个摄像头在地图上的位置(百分比) + const cameraPositions = [ + { cam: 1, x: 25.7, y: 84.3, width: 13.0, height: 8.0 }, + { cam: 2, x: 35.0, y: 56.6, width: 13.0, height: 8.0 }, + { cam: 3, x: 51.5, y: 77.6, width: 13.0, height: 8.0 }, + { cam: 4, x: 57.7, y: 44.9, width: 12.9, height: 8.0 }, + { cam: 5, x: 75.4, y: 60.3, width: 12.9, height: 8.0 }, + { cam: 6, x: 77.2, y: 82.2, width: 13.0, height: 8.0 }, + { cam: 7, x: 52.0, y: 27.9, width: 12.9, height: 8.0 }, + { cam: 8, x: 80.2, y: 21.9, width: 12.8, height: 8.0 }, + { cam: 9, x: 24.4, y: 20.6, width: 12.9, height: 8.0 }, + { cam: 10, x: 7.9, y: 39.1, width: 12.8, height: 8.0 }, + { cam: 11, x: 72.9, y: 4.6, width: 13.0, height: 8.0 }, + ]; + + // 为每个摄像头创建可点击热区 + cameraPositions.forEach(pos => { + const hotspot = document.createElement('div'); + hotspot.className = 'camera-hotspot'; + hotspot.style.position = 'absolute'; + hotspot.style.left = pos.x + '%'; + hotspot.style.top = pos.y + '%'; + hotspot.style.width = pos.width + '%'; + hotspot.style.height = pos.height + '%'; + hotspot.style.cursor = 'pointer'; + hotspot.style.transition = 'all 0.2s'; + hotspot.style.display = 'flex'; + hotspot.style.alignItems = 'center'; + hotspot.style.justifyContent = 'center'; + hotspot.style.fontSize = '0.7vw'; + hotspot.style.fontWeight = 'bold'; + hotspot.style.color = '#fff'; + hotspot.style.textShadow = '1px 1px 2px #000'; + hotspot.style.fontFamily = 'Arial, sans-serif'; + hotspot.style.whiteSpace = 'nowrap'; + hotspot.style.borderRadius = '4px'; + hotspot.style.letterSpacing = '0.5px'; + + // 添加CAM文本 + hotspot.textContent = `CAM ${pos.cam}`; + + // 当前选中的摄像头绿色闪烁 + if (this.game.state.currentCam === `cam${pos.cam}`) { + hotspot.classList.add('camera-selected'); + hotspot.style.border = 'none'; + } else { + hotspot.style.border = 'none'; + hotspot.style.background = 'transparent'; + } + + // 悬浮效果 + hotspot.addEventListener('mouseenter', () => { + if (this.game.state.currentCam !== `cam${pos.cam}`) { + hotspot.style.background = 'rgba(255, 255, 255, 0.2)'; + } + }); + + hotspot.addEventListener('mouseleave', () => { + if (this.game.state.currentCam !== `cam${pos.cam}`) { + hotspot.style.background = 'transparent'; + } + }); + + // 点击切换摄像头 + hotspot.addEventListener('click', () => this.switchCamera(pos.cam)); + + mapContainer.appendChild(hotspot); + }); + + grid.appendChild(mapContainer); + } + + playAmbientSound() { + // 如果在冷却中,不能使用 + if (this.soundButtonCooldown) { + console.log('Sound button on cooldown'); + return; + } + + const currentCam = this.game.state.currentCam; + + // 检查EP是否移动了,如果移动了则重置所有位置的计数 + const currentEpLocation = this.game.enemyAI.getCurrentLocation(); + if (this.lastEpLocation !== currentEpLocation) { + console.log(`EP moved from ${this.lastEpLocation} to ${currentEpLocation}, resetting all location counts`); + this.locationAttractCount = {}; // 重置所有位置计数 + this.lastEpLocation = currentEpLocation; + } + + // 交替播放 1.ogg 和 2.ogg + const soundFile = this.currentSoundToggle ? '2.ogg' : '1.ogg'; + this.currentSoundToggle = !this.currentSoundToggle; + + // 创建并播放音频 + const audio = new Audio(`assets/sounds/${soundFile}`); + audio.play().catch(e => console.log('音频播放失败:', e)); + + // 检查当前位置是否已经用完2次 + let canAttract = true; + if (this.locationAttractCount[currentCam] >= this.maxLocationAttractCount) { + console.log(`Location ${currentCam} already used ${this.maxLocationAttractCount} times - wasting player's attempt`); + canAttract = false; + } + + // 尝试吸引EP到当前摄像头位置(如果位置可用) + let attracted = false; + if (canAttract) { + attracted = this.game.enemyAI.attractToSound(currentCam); + + if (attracted) { + // 吸引成功,播放过场动画 + this.playAttractionTransition(); + + // 增加该位置的计数 + this.locationAttractCount[currentCam] = (this.locationAttractCount[currentCam] || 0) + 1; + console.log(`Epstein attracted to ${currentCam}! Count: ${this.locationAttractCount[currentCam]}/${this.maxLocationAttractCount}`); + + // 更新EP位置记录 + this.lastEpLocation = currentCam; + } else { + // 吸引失败(不邻近或其他原因),不给用户提示 + console.log('Attraction failed'); + } + } else { + // 位置已用完2次,浪费玩家的尝试 + console.log('Location maxed out - player wasted an attempt'); + } + + // 增加使用次数(无论是否成功) + this.soundButtonUseCount++; + console.log(`Sound button used: ${this.soundButtonUseCount}/${this.maxSoundUses}`); + + // 检查是否达到最大使用次数 + if (this.soundButtonUseCount >= this.maxSoundUses) { + console.log('Sound button overused! Camera failure!'); + this.soundButtonUseCount = 0; // 重置计数 + + // 如果正在播放吸引动画,立即停止 + if (this.cameraPanel.classList.contains('transitioning')) { + this.stopStatic(); + this.cameraPanel.classList.remove('transitioning'); + } + + // 触发摄像头故障 + this.game.enemyAI.triggerCameraFailure(); + } + + // 开始冷却 + this.soundButtonCooldown = true; + this.playSoundBtn.style.opacity = '0.5'; + this.playSoundBtn.style.cursor = 'not-allowed'; + + // 添加加载动画 + this.startCooldownAnimation(); + + // 8秒后解除冷却 + setTimeout(() => { + this.soundButtonCooldown = false; + this.playSoundBtn.style.opacity = '1'; + this.playSoundBtn.style.cursor = 'pointer'; + this.stopCooldownAnimation(); + }, this.cooldownTime); + } + + // 开始冷却动画 + startCooldownAnimation() { + let dotCount = 0; + this.cooldownInterval = setInterval(() => { + dotCount = (dotCount + 1) % 4; + const dots = '.'.repeat(dotCount); + this.playSoundBtn.textContent = `PLAY SOUND${dots}`; + }, 500); + } + + // 停止冷却动画 + stopCooldownAnimation() { + if (this.cooldownInterval) { + clearInterval(this.cooldownInterval); + this.cooldownInterval = null; + } + this.playSoundBtn.textContent = 'PLAY SOUND'; + } + + // 吸引成功的过场动画 + playAttractionTransition() { + console.log('Playing attraction transition...'); + + // 添加过场状态,隐藏背景图片和地图 + this.cameraPanel.classList.add('transitioning'); + + // 隐藏地图 + const cameraGrid = document.getElementById('camera-grid'); + if (cameraGrid) { + cameraGrid.style.display = 'none'; + } + + // 隐藏角色 + const characterOverlay = document.getElementById('character-overlay'); + if (characterOverlay) { + characterOverlay.style.display = 'none'; + } + + // 暂时降低循环静态音的音量 + this.game.assets.setSoundVolume('staticLoop', 0.1); + + // 播放正常音量的静态音效 + this.game.assets.playSound('static', false, 1.0); + + // 1000ms 后停止静态音效 + setTimeout(() => { + this.game.assets.stopSound('static'); + }, 1000); + + // 显示雪花效果 + this.startStatic(); + + // 500ms 后更新显示 + setTimeout(() => { + // 如果摄像头已经故障,停止动画并显示故障效果 + if (this.game.state.cameraFailed) { + console.log('Camera failed during attraction transition, showing failure effect'); + this.showCameraFailure(); + return; + } + + this.updateCharacterDisplay(); + + // 再过 500ms 淡出雪花,恢复背景 + setTimeout(() => { + // 如果摄像头已经故障,停止动画并显示故障效果 + if (this.game.state.cameraFailed) { + console.log('Camera failed during attraction transition, showing failure effect'); + this.showCameraFailure(); + return; + } + + this.stopStatic(); + this.cameraPanel.classList.remove('transitioning'); + + // 显示地图 + if (cameraGrid) { + cameraGrid.style.display = 'block'; + } + + // 显示角色 + if (characterOverlay) { + characterOverlay.style.display = 'block'; + } + + // 恢复循环静态音的音量 + this.game.assets.setSoundVolume('staticLoop', 0.3); + }, 500); + }, 500); + } + + // 重置声音按钮计数(摄像头重启后调用) + resetSoundButtonCount() { + this.soundButtonUseCount = 0; + } + + // EP移动时的过场动画 + playMovementTransition() { + console.log('Playing movement transition...'); + + // 如果摄像头已经故障,不播放动画 + if (this.game.state.cameraFailed) { + console.log('Camera already failed, skipping movement transition'); + return; + } + + // 添加过场状态 + this.cameraPanel.classList.add('transitioning'); + + // 隐藏地图 + const cameraGrid = document.getElementById('camera-grid'); + if (cameraGrid) { + cameraGrid.style.display = 'none'; + } + + // 隐藏角色 + const characterOverlay = document.getElementById('character-overlay'); + if (characterOverlay) { + characterOverlay.style.display = 'none'; + } + + // 暂时降低循环静态音的音量 + this.game.assets.setSoundVolume('staticLoop', 0.1); + + // 播放正常音量的静态音效 + this.game.assets.playSound('static', false, 1.0); + + // 1000ms 后停止静态音效 + setTimeout(() => { + this.game.assets.stopSound('static'); + }, 1000); + + // 显示雪花效果 + this.startStatic(); + + // 500ms 后更新显示 + setTimeout(() => { + // 如果摄像头已经故障,停止动画并显示故障效果 + if (this.game.state.cameraFailed) { + console.log('Camera failed during movement transition, showing failure effect'); + this.showCameraFailure(); + return; + } + + this.updateCharacterDisplay(); + + // 再过 500ms 淡出雪花,恢复背景 + setTimeout(() => { + // 如果摄像头已经故障,停止动画并显示故障效果 + if (this.game.state.cameraFailed) { + console.log('Camera failed during movement transition, showing failure effect'); + this.showCameraFailure(); + return; + } + + this.stopStatic(); + this.cameraPanel.classList.remove('transitioning'); + + // 显示地图 + if (cameraGrid) { + cameraGrid.style.display = 'block'; + } + + // 显示角色 + if (characterOverlay) { + characterOverlay.style.display = 'block'; + } + + // 恢复循环静态音的音量 + this.game.assets.setSoundVolume('staticLoop', 0.3); + }, 500); + }, 500); + } + + // 电击霍金 + shockHawking() { + // 立即播放音效 + this.game.assets.playSound('hawking_shock', false, 1.0); + + // 显示雪花过场动画 + this.cameraPanel.classList.add('transitioning'); + + // 播放雪花视频 + if (this.staticVideo) { + this.staticVideo.classList.add('active'); + this.staticVideo.currentTime = 0; + this.staticVideo.play().catch(e => console.log('Video playback failed:', e)); + } + + // 1秒后执行电击并恢复画面 + setTimeout(() => { + if (this.game.enemyAI && this.game.enemyAI.shockHawking()) { + console.log('Hawking shocked successfully!'); + } + + // 停止雪花视频 + if (this.staticVideo) { + this.staticVideo.classList.remove('active'); + this.staticVideo.pause(); + } + + // 恢复摄像头画面 + this.cameraPanel.classList.remove('transitioning'); + this.updateView(); + }, 1000); + } + + // 更新电击按钮显示(Night 3-5 和 Custom Night 中 Hawking 激活时显示) + updateShockButtonVisibility() { + if (this.shockHawkingBtn) { + const currentCam = this.game.state.currentCam; + const night = this.game.state.currentNight; + + // Night 3-5 显示 + const isNormalNight = night >= 3 && night <= 5; + + // Custom Night 且 Hawking AI > 0 时显示 + const isCustomNightWithHawking = this.game.state.customNight && + night === 7 && + this.game.state.customAILevels.hawking > 0; + + if ((isNormalNight || isCustomNightWithHawking) && this.game.state.cameraOpen && currentCam === 'cam6') { + this.shockHawkingBtn.style.display = 'block'; + } else { + this.shockHawkingBtn.style.display = 'none'; + } + } + } + + // 渲染电眼特效(Night 6)- 作为EP容器的子元素 + renderLightningEyes(epContainer, currentCam) { + const eyesConfig = this.game.enemyAI.lightningEyesConfig[currentCam]; + if (!eyesConfig) return; + + // 创建两只眼睛(相对于EP图片定位) + [eyesConfig.eye1, eyesConfig.eye2].forEach((eyeConfig, index) => { + // 眼睛容器 + const eyeContainer = document.createElement('div'); + eyeContainer.className = 'lightning-eye-container'; + eyeContainer.style.position = 'absolute'; + eyeContainer.style.left = eyeConfig.left; + eyeContainer.style.top = eyeConfig.top; + eyeContainer.style.width = eyeConfig.width; + eyeContainer.style.height = eyeConfig.height; + eyeContainer.style.transform = 'translate(-50%, -50%)'; + eyeContainer.style.transformOrigin = 'center center'; + eyeContainer.style.zIndex = '10'; + eyeContainer.style.pointerEvents = 'none'; + + // 核心发光点 + const core = document.createElement('div'); + core.className = 'lightning-eye-core'; + core.style.position = 'absolute'; + core.style.top = '50%'; + core.style.left = '50%'; + core.style.width = '60%'; + core.style.height = '60%'; + core.style.transform = 'translate(-50%, -50%)'; + core.style.background = 'radial-gradient(circle, rgba(255, 255, 255, 1) 0%, rgba(0, 255, 255, 1) 40%, rgba(0, 200, 255, 0.6) 70%, transparent 100%)'; + core.style.borderRadius = '50%'; + core.style.filter = 'brightness(2)'; + core.style.animation = 'lightning-pulse 0.15s infinite'; + + // 外层光晕 + const glow = document.createElement('div'); + glow.className = 'lightning-eye-glow'; + glow.style.position = 'absolute'; + glow.style.top = '50%'; + glow.style.left = '50%'; + glow.style.width = '100%'; + glow.style.height = '100%'; + glow.style.transform = 'translate(-50%, -50%)'; + glow.style.background = 'radial-gradient(ellipse at center, rgba(0, 255, 255, 0.8) 0%, rgba(0, 255, 255, 0.4) 30%, rgba(0, 200, 255, 0.2) 60%, transparent 100%)'; + glow.style.borderRadius = '50%'; + glow.style.boxShadow = ` + 0 0 20px rgba(0, 255, 255, 1), + 0 0 40px rgba(0, 255, 255, 0.8), + 0 0 60px rgba(0, 255, 255, 0.6) + `; + glow.style.animation = 'lightning-flicker 0.1s infinite'; + + // 雷电效果(多条随机闪电) + for (let i = 0; i < 3; i++) { + const lightning = document.createElement('div'); + lightning.className = 'lightning-bolt'; + lightning.style.position = 'absolute'; + lightning.style.top = '50%'; + lightning.style.left = '50%'; + lightning.style.width = '2px'; + lightning.style.height = `${30 + Math.random() * 40}%`; + lightning.style.background = 'linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(0, 255, 255, 0.8), transparent)'; + lightning.style.transformOrigin = 'top center'; + lightning.style.transform = `translate(-50%, -50%) rotate(${Math.random() * 360}deg)`; + lightning.style.boxShadow = '0 0 5px rgba(0, 255, 255, 1), 0 0 10px rgba(0, 255, 255, 0.8)'; + lightning.style.animation = `lightning-bolt ${0.1 + Math.random() * 0.1}s infinite`; + lightning.style.animationDelay = `${Math.random() * 0.1}s`; + lightning.style.opacity = '0.8'; + eyeContainer.appendChild(lightning); + } + + eyeContainer.appendChild(glow); + eyeContainer.appendChild(core); + epContainer.appendChild(eyeContainer); + }); + + console.log(`⚡ Rendered lightning eyes with electric effects at ${currentCam}`); + } +} diff --git a/games/five-nights-at-epsteins/js/EnemyAI.js b/games/five-nights-at-epsteins/js/EnemyAI.js new file mode 100644 index 00000000..2e8eefed --- /dev/null +++ b/games/five-nights-at-epsteins/js/EnemyAI.js @@ -0,0 +1,1819 @@ +// 敌人AI系统 - 基于FNAF机制 +class EnemyAI { + constructor(game) { + this.game = game; + + // ==================== AI配置系统 ==================== + // Epstein AI配置(按夜数) + this.epsteinConfig = { + 1: { + aiLevel: 12, // AI等级 (0-20),12/24 = 50%移动概率 + movementInterval: [9000, 10000], // 移动检查间隔(毫秒)[最小值, 最大值] + movementDuration: 1000, // 移动动画时长(毫秒) + spawnDelay: 120000, // 出场延迟(毫秒) + movementProbability: { // 移动方向概率 + forward: 0.8, // 前进概率 100% + lateral: 0.1, // 平移概率 0%(当前不支持) + backward: 0.1 // 后退概率 0% + }, + soundLureResistance: 0 // 对sound吸引的抵抗概率(0-1) + }, + 2: { + aiLevel: 12, + movementInterval: [9000, 10000], + movementDuration: 1000, + spawnDelay: 0, + movementProbability: { + forward: 0.8, + lateral: 0.1, + backward: 0.1 + }, + soundLureResistance: 0.1 + }, + 3: { + aiLevel: 12, + movementInterval: [9000, 10000], + movementDuration: 1000, + spawnDelay: 0, + movementProbability: { + forward: 0.8, + lateral: 0.2, + backward: 0 + }, + soundLureResistance: 0.1 // Night 3开始:15%概率抵抗sound吸引 + }, + 4: { + aiLevel: 12, + movementInterval: [9000, 10000], // Night 4:9-10秒间隔 + movementDuration: 1000, + spawnDelay: 0, + movementProbability: { + forward: 0.9, + lateral: 0.1, + backward: 0 + }, + soundLureResistance: 0.15 + }, + 5: { + aiLevel: 12, + movementInterval: [9000, 10000], // Night 5:9-10秒间隔 + movementDuration: 1000, + spawnDelay: 0, + movementProbability: { + forward: 0.9, + lateral: 0.1, + backward: 0.0 + }, + soundLureResistance: 0.15 + }, + 6: { + aiLevel: 12, + movementInterval: [7500, 8500], // 7.5-8.5秒间隔 + movementDuration: 1000, + spawnDelay: 0, // Night 6立即出场 + movementProbability: { + forward: 0.85, // 85%前进 + lateral: 0.15, // 15%平移 + backward: 0.0 // 不后退 + }, + soundLureResistance: 0.15 + } + }; + + // Trump AI配置(按夜数,Night 2开始) + this.trumpConfig = { + 2: { + aiLevel: 10, // AI等级,10/24 = 42%移动概率 + movementInterval: [8000, 9000], // 8-9秒随机(比EP快一点) + movementDuration: 1000, // 移动动画时长(毫秒) + spawnDelay: 0, // 出场延迟(毫秒),开局就出场 + movementProbability: { // 移动方向概率 + forward: 0.9, // 90% 前进(更激进) + lateral: 0.1, // 10% 平移 + backward: 0.0 // 0% 后退 + }, + ventCrawling: { // 通风管爬行配置 + cam1Probability: 1.0, // 在cam1时爬行概率 100% + cam2Probability: 0.5, // 在cam2时爬行概率 50% + soundDelay: 5000, // 开始爬行后多久播放音效(毫秒) + soundDuration: 10000, // 爬行音效持续时长(毫秒) + totalDuration: 20000, // 爬行总时长(毫秒) + retreatDelay: 2000, // 被阻止后多久播放撤退音效(毫秒) + retreatSoundDuration: 3000 // 撤退音效持续时长(毫秒) + } + }, + 3: { + aiLevel: 11, // 降低到46%移动概率(从55%) + movementInterval: [9000, 10000], // 改为9-10秒(从8-9秒) + movementDuration: 1000, + spawnDelay: 0, + movementProbability: { + forward: 0.75, // 降低到75%前进(从80%) + lateral: 0.25, // 增加到25%平移 + backward: 0.0 + }, + ventCrawling: { + cam1Probability: 1.0, + cam2Probability: 0.4, // 降低到40%(从50%) + soundDelay: 5000, + soundDuration: 10000, + totalDuration: 20000, + retreatDelay: 2000, + retreatSoundDuration: 3000 + } + }, + 4: { + aiLevel: 13, + movementInterval: [8000, 9000], // 8-9秒随机(比EP快) + movementDuration: 1000, + spawnDelay: 0, + movementProbability: { + forward: 0.8, // 80% 前进 + lateral: 0.2, // 20% 平移 + backward: 0.0 + }, + ventCrawling: { + cam1Probability: 1.0, + cam2Probability: 0.5, + soundDelay: 5000, + soundDuration: 10000, + totalDuration: 20000, + retreatDelay: 2000, + retreatSoundDuration: 3000 + } + }, + 5: { + aiLevel: 13, + movementInterval: [8000, 9000], // 8-9秒随机(比EP快) + movementDuration: 1000, + spawnDelay: 0, + movementProbability: { + forward: 0.8, // 80% 前进 + lateral: 0.2, // 20% 平移 + backward: 0.0 + }, + ventCrawling: { + cam1Probability: 1.0, + cam2Probability: 0.5, + soundDelay: 5000, + soundDuration: 10000, + totalDuration: 20000, + retreatDelay: 2000, + retreatSoundDuration: 3000 + } + } + }; + + // 当前配置(运行时使用) + this.currentEpsteinConfig = null; + this.currentTrumpConfig = null; + + // 爱泼斯坦的状态 + this.epstein = { + currentLocation: 'cam11', // 起始位置(最远) + aiLevel: 0, // AI等级 (0-20, 分母24) + movementTimer: null, + movementInterval: 12000, // 移动检查间隔 + hasMovedOnce: false, // 是否已经移动过一次 + hasSpawned: false, // 是否已经出场 + }; + + // 特朗普的状态 + this.trump = { + currentLocation: 'cam10', // 起始位置 + aiLevel: 0, // AI等级 (0-20) + movementTimer: null, + movementInterval: 10000, // 移动检查间隔 + hasSpawned: false, // 是否已经出场 + isCrawling: false, // 是否正在爬行 + crawlingTimer: null, // 爬行计时器 + crawlingFrom: null, // 从哪个摄像头开始爬行(cam1或cam2) + retreatTimer: null, // 撤退计时器 + }; + + // 霍金的状态(第3关开始) + this.hawking = { + active: false, // 是否激活 + location: 'cam6', // 固定在cam6 + timer: null, // 计时器 + warningLevel: 0, // 0=无警告, 1=黄色警告, 2=红色警告 + warningTimer: null, // 警告计时器 + attackTimer: null, // 攻击计时器 + }; + + // 每个摄像头使用的角色图片(根据距离办公室远近) + this.characterImages = { + 'cam11': '/assets/images/enemyep1.png', + 'cam10': '/assets/images/ep1.png', + 'cam1': '/assets/images/ep4.png', + 'cam9': '/assets/images/enemyep1.png', + 'cam8': '/assets/images/enemyep1.png', + 'cam7': '/assets/images/enemyep1.png', + 'cam6': '/assets/images/enemyep1.png', + 'cam5': '/assets/images/enemyep4.png', + 'cam4': '/assets/images/ep1.png', + 'cam3': '/assets/images/ep4.png', + 'cam2': '/assets/images/enemyep1.png', + }; + + // Night 6 专用图片(带电眼) + this.characterImagesNight6 = { + 'cam11': '/assets/images/enemyep1_night6.png', + 'cam10': '/assets/images/ep1_night6.png', + 'cam1': '/assets/images/ep4_night6.png', + 'cam9': '/assets/images/enemyep1_night6.png', + 'cam8': '/assets/images/enemyep1_night6.png', + 'cam7': '/assets/images/enemyep1_night6.png', + 'cam6': '/assets/images/enemyep1_night6.png', + 'cam5': '/assets/images/enemyep4_night6.png', + 'cam4': '/assets/images/ep1_night6.png', + 'cam3': '/assets/images/ep4_night6.png', + 'cam2': '/assets/images/enemyep1_night6.png', + }; + + // 特朗普的图片配置(使用绝对路径) + this.trumpImages = { + 'cam10': '/assets/images/trump3.png', + 'cam11': '/assets/images/trump3.png', + 'cam9': '/assets/images/trump.png', + 'cam8': '/assets/images/trump5.png', + 'cam7': '/assets/images/trump3.png', + 'cam6': '/assets/images/trump3.png', + 'cam5': '/assets/images/trump2.png', + 'cam1': '/assets/images/trump4.png', + 'cam2': '/assets/images/trump4.png', + 'cam3': '/assets/images/trump2.png', + 'cam4': '/assets/images/trump3.png', + }; + + // 定义移动路径图(根据地图连接关系,只能向前移动) + // 每个位置的步长(距离办公室的最短路径长度)- 使用BFS计算 + // Office ← Cam1 ← Cam3 ← ... + this.locationDepth = { + 'office': 0, // 终点 + 'cam1': 1, // Cam1 → Office (1步) + 'cam2': 2, // Cam2 → Cam1 → Office (2步) ✅ 修正 + 'cam3': 2, // Cam3 → Cam1 → Office (2步) + 'cam6': 3, // Cam6 → Cam3 → Cam1 → Office (3步) + 'cam4': 3, // Cam4 → Cam3 → Cam1 → Office (3步) + 'cam5': 4, // Cam5 → Cam6 → Cam3 → Cam1 → Office (4步) + 'cam7': 4, // Cam7 → Cam4 → Cam3 → Cam1 → Office (4步) + 'cam8': 5, // Cam8 → Cam5 → Cam6 → Cam3 → Cam1 → Office (5步) + 'cam11': 5, // Cam11 → Cam7 → Cam4 → Cam3 → Cam1 → Office (5步) + 'cam9': 5, // Cam9 → Cam7 → Cam4 → Cam3 → Cam1 → Office (5步) + 'cam10': 6, // Cam10 → Cam9 → Cam7 → Cam4 → Cam3 → Cam1 → Office (6步) + }; + + // 特朗普的步长配置(使用和EP相同的步长,一步一步走) + this.trumpLocationDepth = { + 'office': 0, // 终点 + 'cam1': 1, // Cam1 → Office (1步) + 'cam2': 2, // Cam2 → Cam1 → Office (2步) + 'cam3': 2, // Cam3 → Cam1 → Office (2步) + 'cam6': 3, // Cam6 → Cam3 → Cam1 → Office (3步) + 'cam4': 3, // Cam4 → Cam3 → Cam1 → Office (3步) + 'cam5': 4, // Cam5 → Cam6 → Cam3 → Cam1 → Office (4步) + 'cam7': 4, // Cam7 → Cam4 → Cam3 → Cam1 → Office (4步) + 'cam8': 5, // Cam8 → Cam5 → Cam6 → Cam3 → Cam1 → Office (5步) + 'cam11': 5, // Cam11 → Cam7 → Cam4 → Cam3 → Cam1 → Office (5步) + 'cam9': 5, // Cam9 → Cam7 → Cam4 → Cam3 → Cam1 → Office (5步) + 'cam10': 6, // Cam10 → Cam9 → Cam7 → Cam4 → Cam3 → Cam1 → Office (6步) + }; + + // 定义移动路径图(只能向办公室方向移动,不能后退) + this.movementPaths = { + 'cam11': ['cam7', 'cam8'], // Cam11只连接Cam7和Cam8 + 'cam9': ['cam7', 'cam10'], + 'cam10': ['cam9'], // 死胡同,只能去cam9 + 'cam8': ['cam7', 'cam5'], + 'cam7': ['cam4'], + 'cam4': ['cam2', 'cam3'], // 修正:添加cam3 + 'cam5': ['cam4', 'cam6'], + 'cam2': ['cam3', 'cam1'], // 修正:添加cam1 + 'cam3': ['cam1', 'cam6'], + 'cam6': ['cam3'], + 'cam1': ['office'], // 大门,只能进办公室 + 'office': [] // 到达办公室,游戏结束 + }; + + // 每个摄像头的邻近房间(用于sound吸引)- 完整的双向连接 + this.adjacentRooms = { + 'cam11': ['cam7', 'cam8'], + 'cam9': ['cam7', 'cam10'], + 'cam10': ['cam9'], + 'cam8': ['cam11', 'cam7', 'cam5'], + 'cam7': ['cam11', 'cam8', 'cam9', 'cam4'], + 'cam4': ['cam7', 'cam2', 'cam5', 'cam3'], // 修正:添加cam3 + 'cam5': ['cam8', 'cam4', 'cam6'], + 'cam2': ['cam4', 'cam3', 'cam1'], // 修正:添加cam1 + 'cam3': ['cam2', 'cam1', 'cam6', 'cam4'], // 修正:添加cam4 + 'cam6': ['cam5', 'cam3'], + 'cam1': ['cam3', 'cam2'], // 修正:添加cam2(不包括office,因为那是终点) + }; + + // 每个摄像头的角色位置配置(CSS定位) + this.characterPositions = { + 'cam11': { left: '57.1%', bottom: '0%', width: '29%', transform: 'translateX(-50%) rotate(0deg)' }, + 'cam10': { left: '73.8%', bottom: '1.6%', width: '89.2%', transform: 'translateX(-50%) rotate(0deg)' }, + 'cam1': { left: '39.9%', bottom: '35.3%', width: '38.8%', transform: 'translateX(-50%) rotate(0deg)' }, + 'cam9': { left: '18.5%', bottom: '0%', width: '29.6%', transform: 'translateX(-50%) rotate(0deg)' }, + 'cam8': { left: '96.1%', bottom: '0%', width: '29.6%', transform: 'translateX(-50%) rotate(-23deg)' }, + 'cam7': { left: '49.7%', bottom: '0%', width: '29.6%', transform: 'translateX(-50%) rotate(-5deg)' }, + 'cam6': { left: '16.6%', bottom: '0%', width: '29.6%', transform: 'translateX(-50%) rotate(-5deg)' }, + 'cam5': { left: '71.1%', bottom: '0%', width: '29.6%', transform: 'translateX(-50%) rotate(-5deg)' }, + 'cam4': { left: '91.4%', bottom: '6.8%', width: '66.9%', transform: 'translateX(-50%) rotate(-5deg)' }, + 'cam3': { left: '7.4%', bottom: '5%', width: '66.9%', transform: 'translateX(-50%) rotate(-5deg)' }, + 'cam2': { left: '39.6%', bottom: '27.7%', width: '37.8%', transform: 'translateX(-50%) rotate(-139deg)' }, + }; + + // 角色明暗度配置(百分比) + this.characterBrightness = { + 'cam11': 100, + 'cam10': 100, + 'cam1': 22, + 'cam9': 8, + 'cam8': 9, + 'cam7': 9, + 'cam6': 9, + 'cam5': 7, + 'cam4': 65, + 'cam3': 30, + 'cam2': 8, + }; + + // 角色旋转配置(度数) + this.characterRotation = { + 'cam11': 0, + 'cam10': 0, + 'cam1': 0, + 'cam9': 0, + 'cam8': -23, + 'cam7': -5, + 'cam6': -5, + 'cam5': -5, + 'cam4': -5, + 'cam3': -5, + 'cam2': -139, + }; + + // 电眼特效配置(Night 6 特殊夜晚)- 坐标相对于EP图片 + this.lightningEyesConfig = { + 'cam11': { + eye1: { left: '46.3%', top: '14.8%', width: '10%', height: '10%' }, + eye2: { left: '54.2%', top: '13.8%', width: '10%', height: '10%' } + }, + 'cam10': { + eye1: { left: '37.0%', top: '41.7%', width: '10%', height: '10%' }, + eye2: { left: '38.7%', top: '43.5%', width: '10%', height: '10%' } + }, + 'cam1': { + eye1: { left: '47.7%', top: '41.6%', width: '10%', height: '10%' }, + eye2: { left: '49.9%', top: '42.3%', width: '10%', height: '10%' } + }, + 'cam9': { + eye1: { left: '46.8%', top: '15.1%', width: '10%', height: '10%' }, + eye2: { left: '54.7%', top: '14.1%', width: '10%', height: '10%' } + }, + 'cam8': { + eye1: { left: '47.1%', top: '15.8%', width: '10%', height: '10%' }, + eye2: { left: '53.9%', top: '15.3%', width: '10%', height: '10%' } + }, + 'cam7': { + eye1: { left: '46.3%', top: '15.6%', width: '10%', height: '10%' }, + eye2: { left: '54.2%', top: '13.6%', width: '10%', height: '10%' } + }, + 'cam6': { + eye1: { left: '46.8%', top: '15.4%', width: '10%', height: '10%' }, + eye2: { left: '53.7%', top: '14.5%', width: '10%', height: '10%' } + }, + 'cam5': { + eye1: { left: '52.2%', top: '21.3%', width: '10%', height: '10%' }, + eye2: { left: '62.0%', top: '23.1%', width: '10%', height: '10%' } + }, + 'cam4': { + eye1: { left: '37.1%', top: '42.4%', width: '10%', height: '10%' }, + eye2: { left: '38.4%', top: '43.6%', width: '10%', height: '10%' } + }, + 'cam3': { + eye1: { left: '47.7%', top: '41.4%', width: '10%', height: '10%' }, + eye2: { left: '50.0%', top: '42.5%', width: '10%', height: '10%' } + }, + 'cam2': { + eye1: { left: '46.1%', top: '15.3%', width: '10%', height: '10%' }, + eye2: { left: '53.9%', top: '14.3%', width: '10%', height: '10%' } + } + }; + + // 特朗普的位置配置(使用调试工具设置) + this.trumpPositions = { + 'cam10': { left: '10%', bottom: '0%', width: '40%', transform: 'translateX(-50%) rotate(0deg)' }, + 'cam11': { left: '38.2%', bottom: '0%', width: '40%', transform: 'translateX(-50%) rotate(0deg)' }, + 'cam9': { left: '0%', bottom: '34.6%', width: '13.9%', transform: 'translateX(-50%) rotate(44deg)' }, + 'cam8': { left: '1.5%', bottom: '24.5%', width: '20.1%', transform: 'translateX(-50%) rotate(58deg)' }, + 'cam7': { left: '7.4%', bottom: '0%', width: '41.4%', transform: 'translateX(-50%) rotate(1deg)' }, + 'cam6': { left: '86.3%', bottom: '0%', width: '41.4%', transform: 'translateX(-50%) rotate(1deg)' }, + 'cam5': { left: '0%', bottom: '0%', width: '29.3%', transform: 'translateX(-50%) rotate(1deg)' }, + 'cam1': { left: '10.8%', bottom: '15%', width: '31.6%', transform: 'translateX(-50%) rotate(1deg)' }, + 'cam2': { left: '77.2%', bottom: '32.3%', width: '31.6%', transform: 'translateX(-50%) rotate(1deg)' }, + 'cam3': { left: '100%', bottom: '21.4%', width: '32.9%', transform: 'translateX(-50%) rotate(-62deg)' }, + 'cam4': { left: '11%', bottom: '0%', width: '31.6%', transform: 'translateX(-50%) rotate(1deg)' }, + }; + + // 特朗普的明暗度配置 + this.trumpBrightness = { + 'cam10': 31, + 'cam11': 100, + 'cam9': 29, + 'cam8': 28, + 'cam7': 28, + 'cam6': 28, + 'cam5': 12, + 'cam1': 40, + 'cam2': 31, + 'cam3': 19, + 'cam4': 31, + }; + + // 特朗普的旋转配置 + this.trumpRotation = { + 'cam10': 0, + 'cam11': 0, + 'cam9': 44, + 'cam8': 58, + 'cam7': 1, + 'cam6': 1, + 'cam5': 1, + 'cam1': 1, + 'cam2': 1, + 'cam3': -62, + 'cam4': 1, + }; + } + + // 开始AI循环 + start() { + console.log(`🎮 EnemyAI.start() called for Night ${this.game.state.currentNight}`); + + // 根据夜数加载配置并设置AI等级 + this.loadAIConfig(); + + console.log(`Night ${this.game.state.currentNight} - Epstein AI Config:`, this.currentEpsteinConfig); + console.log(`Epstein will spawn in ${this.currentEpsteinConfig.spawnDelay / 1000} seconds...`); + + // 根据配置延迟后EP出场(如果AI等级>0) + if (this.epstein.aiLevel > 0) { + const spawnTimer = setTimeout(() => { + console.log(`⏰ Spawn timer triggered after ${this.currentEpsteinConfig.spawnDelay}ms`); + this.spawnEpstein(); + }, this.currentEpsteinConfig.spawnDelay); + + console.log(`⏰ Spawn timer created:`, spawnTimer); + } else { + console.log('Epstein AI level is 0, not spawning'); + } + + // Trump出场逻辑 + if (this.currentTrumpConfig && this.trump.aiLevel > 0) { + console.log(`Night ${this.game.state.currentNight} - Trump AI Config:`, this.currentTrumpConfig); + console.log(`Trump will spawn in ${this.currentTrumpConfig.spawnDelay / 1000} seconds...`); + + // 根据配置延迟后Trump出场 + setTimeout(() => { + this.spawnTrump(); + }, this.currentTrumpConfig.spawnDelay); + } + + // Hawking激活逻辑 + // Custom Night: 根据自定义等级决定是否激活 + if (this.game.state.customNight && this.game.state.customAILevels.hawking > 0) { + console.log('Custom Night: Hawking activated at cam6!'); + this.startHawking(); + } + // 普通夜晚:Night 3-5 激活 + else if (!this.game.state.customNight && this.game.state.currentNight >= 3 && this.game.state.currentNight <= 5) { + console.log('Hawking activated at cam6!'); + this.startHawking(); + } + } + + // 加载AI配置 + loadAIConfig() { + const night = this.game.state.currentNight; + + // Custom Night (Night 7) - 使用自定义AI等级 + if (this.game.state.customNight && night === 7) { + const customLevels = this.game.state.customAILevels; + + // Epstein 自定义配置 + this.currentEpsteinConfig = { + aiLevel: customLevels.epstein, + movementInterval: [9000, 10000], + movementDuration: 1000, + spawnDelay: 0, + movementProbability: { + forward: 0.9, + lateral: 0.1, + backward: 0.0 + }, + soundLureResistance: 0.15 + }; + this.epstein.aiLevel = customLevels.epstein; + this.epstein.movementInterval = this.getRandomInterval(this.currentEpsteinConfig.movementInterval); + + // Trump 自定义配置 + if (customLevels.trump > 0) { + this.currentTrumpConfig = { + aiLevel: customLevels.trump, + movementInterval: [8000, 9000], + movementDuration: 1000, + spawnDelay: 0, + movementProbability: { + forward: 0.8, + lateral: 0.2, + backward: 0.0 + }, + ventCrawling: { + cam1Probability: 1.0, + cam2Probability: 0.5, + soundDelay: 5000, + soundDuration: 10000, + totalDuration: 20000, + retreatDelay: 2000, + retreatSoundDuration: 3000 + } + }; + this.trump.aiLevel = customLevels.trump; + this.trump.movementInterval = this.getRandomInterval(this.currentTrumpConfig.movementInterval); + } else { + this.currentTrumpConfig = null; + } + + // Hawking 自定义配置(通过 customAILevels.hawking 控制) + // Hawking 的 AI 等级不影响移动,只影响是否激活 + + console.log(`Custom Night AI Config loaded:`); + console.log(`- Epstein: Level ${this.epstein.aiLevel}`); + console.log(`- Trump: Level ${this.trump.aiLevel || 0}`); + console.log(`- Hawking: Level ${customLevels.hawking}`); + + return; + } + + // 普通夜晚配置 + // 加载Epstein配置 + this.currentEpsteinConfig = this.epsteinConfig[night] || this.epsteinConfig[1]; + this.epstein.aiLevel = this.currentEpsteinConfig.aiLevel; + this.epstein.movementInterval = this.getRandomInterval(this.currentEpsteinConfig.movementInterval); + + // 加载Trump配置(Night 2-5) + if (night >= 2 && night <= 5) { + this.currentTrumpConfig = this.trumpConfig[night] || this.trumpConfig[2]; + this.trump.aiLevel = this.currentTrumpConfig.aiLevel; + this.trump.movementInterval = this.getRandomInterval(this.currentTrumpConfig.movementInterval); + } else { + this.currentTrumpConfig = null; // Night 6 没有 Trump + } + + console.log(`AI Config loaded for Night ${night}`); + console.log(`- Epstein: Level ${this.epstein.aiLevel}, Interval ${this.epstein.movementInterval}ms`); + if (this.currentTrumpConfig) { + console.log(`- Trump: Level ${this.trump.aiLevel}, Interval ${this.trump.movementInterval}ms`); + } else { + console.log(`- Trump: Not active this night`); + } + } + + // 从区间中随机选择一个间隔时间 + getRandomInterval(intervalConfig) { + // 如果是数组,从区间中随机选择 + if (Array.isArray(intervalConfig)) { + const [min, max] = intervalConfig; + return Math.floor(Math.random() * (max - min + 1)) + min; + } + // 如果是数字,直接返回 + return intervalConfig; + } + + // EP出场 + spawnEpstein() { + if (this.epstein.hasSpawned) return; + + this.epstein.hasSpawned = true; + console.log('✅ Epstein has spawned!'); + + // 第一关触发摄像头故障,第二关及以后不触发 + if (this.game.state.currentNight === 1) { + this.triggerCameraFailure(); + } + + // 开始移动检查循环 + this.startMovementLoop(); + } + + // Trump出场 + spawnTrump() { + if (this.trump.hasSpawned) return; + + this.trump.hasSpawned = true; + console.log('Trump has spawned at cam10!'); + + // 立即更新摄像头显示(如果摄像头打开) + if (this.game.state.cameraOpen) { + this.updateCameraDisplay(); + } + + // 开始Trump的移动检查循环 + this.startTrumpMovementLoop(); + } + + // 停止AI + stop() { + if (this.epstein.movementTimer) { + clearTimeout(this.epstein.movementTimer); // 改为 clearTimeout + this.epstein.movementTimer = null; + } + if (this.trump.movementTimer) { + clearTimeout(this.trump.movementTimer); // 改为 clearTimeout + this.trump.movementTimer = null; + } + if (this.trump.crawlingTimer) { + clearTimeout(this.trump.crawlingTimer); + this.trump.crawlingTimer = null; + } + if (this.trump.retreatTimer) { + clearTimeout(this.trump.retreatTimer); + this.trump.retreatTimer = null; + } + if (this.hawking.timer) { + clearTimeout(this.hawking.timer); + this.hawking.timer = null; + } + if (this.hawking.warningTimer) { + clearTimeout(this.hawking.warningTimer); + this.hawking.warningTimer = null; + } + if (this.hawking.attackTimer) { + clearTimeout(this.hawking.attackTimer); + this.hawking.attackTimer = null; + } + // 停止爬行音效 + this.game.assets.stopSound('ventCrawling'); + // 隐藏霍金警告 + this.hideHawkingWarning(); + } + + // 开始移动检查循环 + startMovementLoop() { + // 使用 setTimeout 而不是 setInterval,以支持动态间隔 + const scheduleNextCheck = () => { + // Night 4特殊机制:4AM后EP变得更激进 + let currentConfig = this.currentEpsteinConfig; + if (this.game.state.currentNight === 4 && this.game.state.currentTime >= 4) { + // 4AM后使用更激进的配置 + currentConfig = { + ...this.currentEpsteinConfig, + movementInterval: [8000, 10000], + movementProbability: { + forward: 1.0, + lateral: 0.0, + backward: 0.0 + } + }; + } + + // 从配置区间中随机选择下一次检查的间隔 + const nextInterval = this.getRandomInterval(currentConfig.movementInterval); + + this.epstein.movementTimer = setTimeout(() => { + this.checkMovement(); + // 递归调度下一次检查 + scheduleNextCheck(); + }, nextInterval); + }; + + // 开始第一次调度 + scheduleNextCheck(); + } + + // 开始Trump的移动检查循环 + startTrumpMovementLoop() { + // 使用 setTimeout 而不是 setInterval,以支持动态间隔 + const scheduleNextCheck = () => { + // Night 5特殊机制:4AM后Trump变得更激进 + let currentConfig = this.currentTrumpConfig; + if (this.game.state.currentNight === 5 && this.game.state.currentTime >= 4) { + // 4AM后使用更激进的配置 + currentConfig = { + ...this.currentTrumpConfig, + movementInterval: [6000, 7000], // 6-7秒间隔 + movementProbability: { + forward: 1.0, + lateral: 0.0, + backward: 0.0 + } + }; + } + + // 从配置区间中随机选择下一次检查的间隔 + const nextInterval = this.getRandomInterval(currentConfig.movementInterval); + + this.trump.movementTimer = setTimeout(() => { + this.checkTrumpMovement(); + // 递归调度下一次检查 + scheduleNextCheck(); + }, nextInterval); + }; + + // 开始第一次调度 + scheduleNextCheck(); + } + + // 检查是否移动(FNAF机制) + checkMovement() { + // 如果还未出场,不移动 + if (!this.epstein.hasSpawned) return; + + // 如果AI等级为0,不移动 + if (this.epstein.aiLevel === 0) return; + + // 如果已经在办公室,不再移动 + if (this.epstein.currentLocation === 'office') return; + + // Custom Night 使用 1-24,普通夜晚使用 1-20 + const maxRandom = (this.game.state.customNight && this.game.state.currentNight === 7) ? 24 : 20; + const randomNumber = Math.floor(Math.random() * maxRandom) + 1; + + // 如果随机数 <= AI等级,移动成功 + if (randomNumber <= this.epstein.aiLevel) { + this.moveToNextLocation(); + } + } + + // 检查Trump是否移动 + checkTrumpMovement() { + // 如果还未出场,不移动 + if (!this.trump.hasSpawned) return; + + // 如果AI等级为0,不移动 + if (this.trump.aiLevel === 0) return; + + // 如果已经在办公室,不再移动 + if (this.trump.currentLocation === 'office') return; + + // Custom Night 使用 1-24,普通夜晚使用 1-20 + const maxRandom = (this.game.state.customNight && this.game.state.currentNight === 7) ? 24 : 20; + const randomNumber = Math.floor(Math.random() * maxRandom) + 1; + + // 如果随机数 <= AI等级,移动成功 + if (randomNumber <= this.trump.aiLevel) { + this.moveTrumpToNextLocation(); + } + } + + // 移动到下一个位置(支持前进、平移、后退) + moveToNextLocation() { + const currentLoc = this.epstein.currentLocation; + const currentDepth = this.locationDepth[currentLoc]; + + // Night 4特殊机制:4AM后EP变得更激进 + let config = this.currentEpsteinConfig; + if (this.game.state.currentNight === 4 && this.game.state.currentTime >= 4) { + config = { + ...this.currentEpsteinConfig, + movementProbability: { + forward: 1.0, + lateral: 0.0, + backward: 0.0 + } + }; + // 只在第一次触发时显示日志 + if (!this.epstein.night4AggressiveMode) { + console.log('⚡ Night 4: 4AM reached! EP is now in aggressive mode (forward only)'); + this.epstein.night4AggressiveMode = true; + } + } + + // 获取所有位置(不限于邻居) + const allLocations = Object.keys(this.locationDepth).filter(loc => + loc !== 'office' && loc !== currentLoc + ); + + // 获取邻近房间 + const adjacentLocs = this.adjacentRooms[currentLoc] || []; + + // 分类所有可能的移动位置 + // 前进:所有步长减1的位置(不限于邻近) + const forwardLocations = allLocations.filter(loc => this.locationDepth[loc] === currentDepth - 1); + // 平移:只能移动到邻近房间且步长相同的位置 + const lateralLocations = adjacentLocs.filter(loc => this.locationDepth[loc] === currentDepth); + // 后退:只能移动到邻近房间且步长加1的位置 + const backwardLocations = adjacentLocs.filter(loc => this.locationDepth[loc] === currentDepth + 1); + + // 如果当前步长是1且没有前进位置,尝试移动到office + if (forwardLocations.length === 0 && currentDepth === 1) { + console.log(`Epstein moved: ${currentLoc} -> office`); + this.epstein.currentLocation = 'office'; + this.triggerJumpscare('epstein'); + return; + } + + // 根据配置概率决定移动方向 + const movementProb = config.movementProbability; + const totalProb = movementProb.forward + movementProb.lateral + movementProb.backward; + + // 如果总概率为0或没有任何可移动位置,不移动 + if (totalProb === 0 || (forwardLocations.length === 0 && lateralLocations.length === 0 && backwardLocations.length === 0)) { + console.log(`Epstein has no valid path from ${currentLoc}`); + return; + } + + // 归一化概率(确保总和为1) + const normalizedProb = { + forward: movementProb.forward / totalProb, + lateral: movementProb.lateral / totalProb, + backward: movementProb.backward / totalProb + }; + + // 根据概率选择移动方向 + const random = Math.random(); + let selectedLocations = []; + let movementType = ''; + + if (random < normalizedProb.forward && forwardLocations.length > 0) { + // 前进 + selectedLocations = forwardLocations; + movementType = 'forward'; + } else if (random < normalizedProb.forward + normalizedProb.lateral && lateralLocations.length > 0) { + // 平移 + selectedLocations = lateralLocations; + movementType = 'lateral'; + } else if (backwardLocations.length > 0) { + // 后退 + selectedLocations = backwardLocations; + movementType = 'backward'; + } else { + // 如果选中的方向没有可用位置,回退到前进(默认行为) + if (forwardLocations.length > 0) { + selectedLocations = forwardLocations; + movementType = 'forward (fallback)'; + } else if (lateralLocations.length > 0) { + selectedLocations = lateralLocations; + movementType = 'lateral (fallback)'; + } else if (backwardLocations.length > 0) { + selectedLocations = backwardLocations; + movementType = 'backward (fallback)'; + } else { + console.log(`Epstein has no valid path from ${currentLoc}`); + return; + } + } + + // 从选中的方向中随机选择一个位置 + const nextLocation = selectedLocations[Math.floor(Math.random() * selectedLocations.length)]; + + console.log(`Epstein moved [${movementType}]: ${currentLoc} (depth ${currentDepth}) -> ${nextLocation} (depth ${this.locationDepth[nextLocation]})`); + + this.epstein.currentLocation = nextLocation; + + // 播放移动音效 + this.game.assets.playSound('blip', false, 0.5); + + // 如果到达办公室,触发游戏结束 + if (nextLocation === 'office') { + this.triggerJumpscare('epstein'); + return; + } + + // 如果摄像头打开且没有故障,无论查看哪个摄像头都播放动画 + if (this.game.state.cameraOpen && !this.game.state.cameraFailed) { + this.game.camera.playMovementTransition(); + } + + // 更新摄像头显示 + this.updateCameraDisplay(); + } + + // Trump移动到下一个位置(支持前进、平移、后退) + moveTrumpToNextLocation() { + const currentLoc = this.trump.currentLocation; + const currentDepth = this.trumpLocationDepth[currentLoc]; + + // Night 5特殊机制:4AM后Trump变得更激进 + let config = this.currentTrumpConfig; + if (this.game.state.currentNight === 5 && this.game.state.currentTime >= 4) { + config = { + ...this.currentTrumpConfig, + movementProbability: { + forward: 1.0, + lateral: 0.0, + backward: 0.0 + }, + ventCrawling: { + ...this.currentTrumpConfig.ventCrawling, + cam1Probability: 1.0, // 4AM后在cam1必定爬行 + cam2Probability: 0.8 // 4AM后在cam2有80%概率爬行 + } + }; + // 只在第一次触发时显示日志 + if (!this.trump.night5AggressiveMode) { + console.log('⚡ Night 5: 4AM reached! Trump is now in aggressive mode (faster + more crawling)'); + this.trump.night5AggressiveMode = true; + } + } + + // 如果正在爬行,不能移动 + if (this.trump.isCrawling) { + console.log('Trump is crawling, cannot move'); + return; + } + + // 如果在cam1,根据配置概率决定是否爬行 + if (currentLoc === 'cam1') { + const shouldCrawl = Math.random() < config.ventCrawling.cam1Probability; + if (shouldCrawl) { + console.log(`Trump starting to crawl from ${currentLoc} to office (${config.ventCrawling.cam1Probability * 100}% chance)`); + this.startTrumpCrawling(currentLoc); + return; + } + } + + // 如果在cam2,根据配置概率决定是否爬行 + if (currentLoc === 'cam2') { + const shouldCrawl = Math.random() < config.ventCrawling.cam2Probability; + if (shouldCrawl) { + console.log(`Trump decided to crawl from ${currentLoc} to office (${config.ventCrawling.cam2Probability * 100}% chance)`); + this.startTrumpCrawling(currentLoc); + return; + } else { + console.log(`Trump decided to continue moving from ${currentLoc} (${(1 - config.ventCrawling.cam2Probability) * 100}% chance)`); + // 继续执行正常移动逻辑 + } + } + + // 获取Trump可以去的位置 + const allLocations = Object.keys(this.trumpLocationDepth).filter(loc => + loc !== 'office' && loc !== currentLoc + ); + + // 获取邻近房间 + const adjacentLocs = this.adjacentRooms[currentLoc] || []; + + // 分类所有可能的移动位置 + // 前进:所有步长减1的位置(不限于邻近) + const forwardLocations = allLocations.filter(loc => this.trumpLocationDepth[loc] === currentDepth - 1); + // 平移:只能移动到邻近房间且步长相同的位置 + const lateralLocations = adjacentLocs.filter(loc => this.trumpLocationDepth[loc] === currentDepth); + // 后退:只能移动到邻近房间且步长加1的位置 + const backwardLocations = adjacentLocs.filter(loc => this.trumpLocationDepth[loc] === currentDepth + 1); + + // 如果没有任何可移动位置,不移动 + if (forwardLocations.length === 0 && lateralLocations.length === 0 && backwardLocations.length === 0) { + console.log(`Trump has no valid path from ${currentLoc}`); + return; + } + + // 根据配置概率决定移动方向 + const movementProb = config.movementProbability; + const totalProb = movementProb.forward + movementProb.lateral + movementProb.backward; + + // 如果总概率为0,不移动 + if (totalProb === 0) { + console.log(`Trump movement probability is 0`); + return; + } + + // 归一化概率 + const normalizedProb = { + forward: movementProb.forward / totalProb, + lateral: movementProb.lateral / totalProb, + backward: movementProb.backward / totalProb + }; + + // 根据概率选择移动方向 + const random = Math.random(); + let selectedLocations = []; + let movementType = ''; + + if (random < normalizedProb.forward && forwardLocations.length > 0) { + selectedLocations = forwardLocations; + movementType = 'forward'; + } else if (random < normalizedProb.forward + normalizedProb.lateral && lateralLocations.length > 0) { + selectedLocations = lateralLocations; + movementType = 'lateral'; + } else if (backwardLocations.length > 0) { + selectedLocations = backwardLocations; + movementType = 'backward'; + } else { + // 回退到可用的方向 + if (forwardLocations.length > 0) { + selectedLocations = forwardLocations; + movementType = 'forward (fallback)'; + } else if (lateralLocations.length > 0) { + selectedLocations = lateralLocations; + movementType = 'lateral (fallback)'; + } else if (backwardLocations.length > 0) { + selectedLocations = backwardLocations; + movementType = 'backward (fallback)'; + } else { + console.log(`Trump has no valid path from ${currentLoc}`); + return; + } + } + + // 从选中的方向中随机选择一个位置 + const nextLocation = selectedLocations[Math.floor(Math.random() * selectedLocations.length)]; + + console.log(`Trump moved [${movementType}]: ${currentLoc} (depth ${currentDepth}) -> ${nextLocation} (depth ${this.trumpLocationDepth[nextLocation]})`); + + this.trump.currentLocation = nextLocation; + + // 播放移动音效 + this.game.assets.playSound('blip', false, 0.5); + + // 如果摄像头打开且没有故障,播放动画 + if (this.game.state.cameraOpen && !this.game.state.cameraFailed) { + this.game.camera.playMovementTransition(); + } + + // 更新摄像头显示 + this.updateCameraDisplay(); + } + + // Trump开始爬行进入办公室 + startTrumpCrawling(fromLocation) { + const config = this.currentTrumpConfig.ventCrawling; + + // 检查通风管是否已经关闭 + if (this.game.state.ventsClosed) { + console.log('Trump tried to crawl but vents are already closed! Silent retreat.'); + + // 静默撤退 - 不播放任何音效 + // 找出所有步长为3的位置 + const depth3Locations = Object.keys(this.trumpLocationDepth).filter(loc => + this.trumpLocationDepth[loc] === 3 && loc !== 'office' + ); + + // 如果没有步长为3的位置,使用EP的步长3位置 + let retreatLocation; + if (depth3Locations.length > 0) { + retreatLocation = depth3Locations[Math.floor(Math.random() * depth3Locations.length)]; + } else { + const epDepth3Locations = Object.keys(this.locationDepth).filter(loc => + this.locationDepth[loc] === 3 && loc !== 'office' + ); + retreatLocation = epDepth3Locations[Math.floor(Math.random() * epDepth3Locations.length)]; + } + + console.log(`Trump silently retreats to ${retreatLocation} (depth 3)`); + + // 直接移动到撤退位置,不播放音效 + this.trump.currentLocation = retreatLocation; + this.trump.isCrawling = false; + this.trump.crawlingFrom = null; + + // 更新摄像头显示 + this.updateCameraDisplay(); + return; + } + + this.trump.isCrawling = true; + this.trump.crawlingFrom = fromLocation; // 记录从哪里开始爬行 + this.trump.currentLocation = 'crawling'; // 标记为爬行状态 + + console.log(`Trump is crawling from ${fromLocation}...`); + console.log(`Crawling config: soundDelay=${config.soundDelay}ms, soundDuration=${config.soundDuration}ms, totalDuration=${config.totalDuration}ms`); + + // 更新摄像头显示(Trump消失) + this.updateCameraDisplay(); + + // 根据配置延迟后开始播放爬行音效 + setTimeout(() => { + // 检查Trump是否还在爬行(可能已经被阻止) + if (this.trump.isCrawling && this.trump.currentLocation === 'crawling') { + console.log('Playing crawling sound...'); + this.game.assets.playSound('ventCrawling', true, 1.0); + + // 根据配置持续时长后停止爬行音效 + setTimeout(() => { + if (this.trump.isCrawling && this.trump.currentLocation === 'crawling') { + console.log('Stopping crawling sound...'); + this.game.assets.stopSound('ventCrawling'); + } + }, config.soundDuration); + } + }, config.soundDelay); + + // 根据配置总时长后到达办公室并触发跳杀 + this.trump.crawlingTimer = setTimeout(() => { + console.log('Trump reached the office!'); + this.trump.currentLocation = 'office'; + this.trump.isCrawling = false; + this.trump.crawlingFrom = null; + + // 停止爬行音效(如果还在播放) + this.game.assets.stopSound('ventCrawling'); + + // 触发跳杀 + this.triggerJumpscare('trump'); + }, config.totalDuration); + } + + // 阻止Trump爬行(玩家关闭通风口) + stopTrumpCrawling() { + if (!this.trump.isCrawling) { + return false; + } + + const config = this.currentTrumpConfig.ventCrawling; + + console.log('Trump crawling blocked by closed vents!'); + + // 清除爬行计时器 + if (this.trump.crawlingTimer) { + clearTimeout(this.trump.crawlingTimer); + this.trump.crawlingTimer = null; + } + + // 停止爬行音效 + this.game.assets.stopSound('ventCrawling'); + + // Trump被阻止,立即判定离开 + this.trump.isCrawling = false; + + // 找出所有步长为3的位置 + const depth3Locations = Object.keys(this.trumpLocationDepth).filter(loc => + this.trumpLocationDepth[loc] === 3 && loc !== 'office' + ); + + // 如果没有步长为3的位置,使用EP的步长3位置 + let retreatLocation; + if (depth3Locations.length > 0) { + // 随机选择一个步长为3的位置 + retreatLocation = depth3Locations[Math.floor(Math.random() * depth3Locations.length)]; + } else { + // 使用EP的步长配置中的步长3位置 + const epDepth3Locations = Object.keys(this.locationDepth).filter(loc => + this.locationDepth[loc] === 3 && loc !== 'office' + ); + retreatLocation = epDepth3Locations[Math.floor(Math.random() * epDepth3Locations.length)]; + } + + console.log(`Trump will retreat to ${retreatLocation} (depth 3)`); + + // 立即移动到撤退位置 + this.trump.currentLocation = retreatLocation; + this.trump.crawlingFrom = null; + + // 更新摄像头显示 + this.updateCameraDisplay(); + + // 根据配置延迟后播放撤退音效 + setTimeout(() => { + console.log('Playing retreat crawling sound...'); + this.game.assets.playSound('ventCrawling', false, 1.0); + + // 根据配置持续时长后停止音效 + setTimeout(() => { + this.game.assets.stopSound('ventCrawling'); + }, config.retreatSoundDuration); + }, config.retreatDelay); + + return true; + } + + // 检查通风口状态变化(从Game.js调用) + onVentsChanged(ventsClosed) { + // 如果Trump正在爬行且玩家关闭了通风口,阻止Trump + if (this.trump.isCrawling && ventsClosed) { + this.stopTrumpCrawling(); + } + } + + // 使用sound吸引敌人(从CameraSystem调用) + attractToSound(soundLocation) { + let epAttracted = false; + let trumpAttracted = false; + + // 尝试吸引EP + const epCurrentLoc = this.epstein.currentLocation; + const adjacentToEp = this.adjacentRooms[epCurrentLoc]; + + if (this.epstein.hasSpawned && adjacentToEp && adjacentToEp.includes(soundLocation)) { + // 根据配置检查是否抵抗sound吸引 + const resistance = this.currentEpsteinConfig.soundLureResistance; + if (resistance > 0) { + const failChance = Math.random(); + if (failChance < resistance) { + console.log(`Epstein resisted the sound lure! (${resistance * 100}% chance on Night ${this.game.state.currentNight})`); + // 吸引失败,但仍然播放音效让玩家以为成功了 + this.game.assets.playSound('blip', false, 0.5); + return false; // 返回false表示没有真正吸引到 + } + } + + // 吸引成功,EP移动到sound位置(可以前进或后退) + const currentDepth = this.locationDepth[epCurrentLoc]; + const soundDepth = this.locationDepth[soundLocation]; + console.log(`Epstein attracted by sound: ${epCurrentLoc} (depth ${currentDepth}) -> ${soundLocation} (depth ${soundDepth})`); + + this.epstein.currentLocation = soundLocation; + + // 播放移动音效 + this.game.assets.playSound('blip', false, 0.5); + + // 如果到达办公室,触发游戏结束 + if (soundLocation === 'office') { + this.triggerJumpscare('epstein'); + } + + epAttracted = true; + } else { + console.log(`Sound at ${soundLocation} is not adjacent to Epstein at ${epCurrentLoc}`); + } + + // 尝试吸引Trump(如果已出场且不在爬行状态) + const trumpCurrentLoc = this.trump.currentLocation; + const adjacentToTrump = this.adjacentRooms[trumpCurrentLoc]; + + if (this.trump.hasSpawned && !this.trump.isCrawling && adjacentToTrump && adjacentToTrump.includes(soundLocation)) { + // 吸引成功,Trump移动到sound位置(可以前进或后退) + const currentDepth = this.trumpLocationDepth[trumpCurrentLoc]; + const soundDepth = this.trumpLocationDepth[soundLocation]; + console.log(`Trump attracted by sound: ${trumpCurrentLoc} (depth ${currentDepth}) -> ${soundLocation} (depth ${soundDepth})`); + + this.trump.currentLocation = soundLocation; + + // 播放移动音效(如果EP没有播放) + if (!epAttracted) { + this.game.assets.playSound('blip', false, 0.5); + } + + // 如果到达办公室,触发游戏结束 + if (soundLocation === 'office') { + this.triggerJumpscare('trump'); + } + + trumpAttracted = true; + } else if (this.trump.hasSpawned && !this.trump.isCrawling) { + console.log(`Sound at ${soundLocation} is not adjacent to Trump at ${trumpCurrentLoc}`); + } + + // 注意:不在这里更新显示,由CameraSystem的动画处理 + + return epAttracted || trumpAttracted; + } + + // 触发摄像头故障 + triggerCameraFailure() { + console.log('Camera system failure!'); + this.game.state.cameraFailed = true; + + // 播放静态噪音 + this.game.assets.playSound('static', true, 1.0); + + // 如果摄像头打开,立即显示故障效果 + if (this.game.state.cameraOpen) { + this.game.camera.showCameraFailure(); + } + // 如果摄像头没打开,下次打开时会自动显示故障效果(在open()方法中) + } + + // 更新摄像头显示 + updateCameraDisplay() { + // 直接调用 CameraSystem 的显示方法 + if (this.game.camera) { + this.game.camera.updateCharacterDisplay(); + } + } + + // 触发跳杀 + triggerJumpscare(enemy = 'epstein') { + console.log(`JUMPSCARE by ${enemy}!`); + this.stop(); + + // 霍金的特殊跳杀动画 + if (enemy === 'hawking') { + this.triggerHawkingMissileJumpscare(); + return; + } + + // 停止所有音效 + this.game.assets.stopSound('vents'); + this.game.assets.stopSound('static'); + + // 创建跳杀动画容器 + const jumpscareContainer = document.createElement('div'); + jumpscareContainer.id = 'jumpscare-container'; + jumpscareContainer.style.position = 'fixed'; + jumpscareContainer.style.top = '0'; + jumpscareContainer.style.left = '0'; + jumpscareContainer.style.width = '100%'; + jumpscareContainer.style.height = '100%'; + jumpscareContainer.style.display = 'flex'; + jumpscareContainer.style.alignItems = 'center'; + jumpscareContainer.style.justifyContent = 'center'; + jumpscareContainer.style.zIndex = '99999'; + jumpscareContainer.style.overflow = 'hidden'; + + // 创建办公室背景 + const officeBackground = document.createElement('img'); + officeBackground.src = this.game.assets.images.office.src; + officeBackground.style.position = 'absolute'; + officeBackground.style.top = '0'; + officeBackground.style.left = '0'; + officeBackground.style.width = '100%'; + officeBackground.style.height = '100%'; + officeBackground.style.objectFit = 'cover'; + officeBackground.style.zIndex = '1'; + + // 创建跳杀图片(居中显示) + const jumpscareImg = document.createElement('img'); + // 根据敌人选择跳杀图片 + if (enemy === 'trump') { + jumpscareImg.src = this.game.assets.images.trumpJumpscare?.src || this.game.assets.images.jumpscare.src; + } else if (enemy === 'hawking') { + jumpscareImg.src = this.game.assets.images.hawkingJumpscare?.src || this.game.assets.images.jumpscare.src; + } else { + jumpscareImg.src = this.game.assets.images.jumpscare.src; + } + jumpscareImg.style.position = 'absolute'; + jumpscareImg.style.top = '50%'; + jumpscareImg.style.left = '50%'; + jumpscareImg.style.transform = 'translate(-50%, -50%)'; + jumpscareImg.style.width = '25%'; // 初始大小25% + jumpscareImg.style.height = 'auto'; + jumpscareImg.style.zIndex = '2'; + jumpscareImg.style.transition = 'none'; + + jumpscareContainer.appendChild(officeBackground); + jumpscareContainer.appendChild(jumpscareImg); + document.body.appendChild(jumpscareContainer); + + // 播放跳杀音效 + if (enemy === 'hawking') { + this.game.assets.playSound('hawkingJumpscare', false, 1.0); + } else { + this.game.assets.playSound('jumpscare', false, 1.0); + } + + // 第1帧:25% (立即显示) + // 已经设置 + + // 第2帧:50% (0.15秒后) + setTimeout(() => { + jumpscareImg.style.width = '50%'; + }, 150); + + // 第3帧:100% (0.3秒后) + setTimeout(() => { + jumpscareImg.style.width = '100%'; + }, 300); + + // 1.5秒后淡出并显示游戏结束画面 + setTimeout(() => { + jumpscareContainer.style.transition = 'opacity 0.5s'; + jumpscareContainer.style.opacity = '0'; + + setTimeout(() => { + document.body.removeChild(jumpscareContainer); + this.game.gameOver('GAME OVER'); + }, 500); + }, 1500); + } + + // 获取当前位置 + getCurrentLocation() { + return this.epstein.currentLocation; + } + + // 获取当前EP的图片(根据夜数选择是否带电眼) + getCurrentImage(cam, night) { + if (night === 6 && this.characterImagesNight6[cam]) { + return this.characterImagesNight6[cam]; + } + return this.characterImages[cam]; + } + + // 获取Trump当前位置 + getTrumpCurrentLocation() { + return this.trump.currentLocation; + } + + // 检查Trump是否正在爬行 + isTrumpCrawling() { + return this.trump.isCrawling; + } + + // 重置AI + reset() { + this.stop(); + + // 重置 Epstein + this.epstein.currentLocation = 'cam11'; + this.epstein.aiLevel = 0; + this.epstein.hasMovedOnce = false; + this.epstein.hasSpawned = false; + this.epstein.night4AggressiveMode = false; // 重置Night 4激进模式标志 + if (this.epstein.timer) { + clearTimeout(this.epstein.timer); + this.epstein.timer = null; + } + + // 重置 Trump + this.trump.currentLocation = 'cam10'; + this.trump.aiLevel = 0; + this.trump.hasSpawned = false; + this.trump.isCrawling = false; + this.trump.crawlingFrom = null; + this.trump.night5AggressiveMode = false; // 重置Night 5激进模式标志 + if (this.trump.timer) { + clearTimeout(this.trump.timer); + this.trump.timer = null; + } + if (this.trump.crawlingTimer) { + clearTimeout(this.trump.crawlingTimer); + this.trump.crawlingTimer = null; + } + if (this.trump.retreatTimer) { + clearTimeout(this.trump.retreatTimer); + this.trump.retreatTimer = null; + } + + // 重置 Hawking + this.hawking.active = false; + this.hawking.warningLevel = 0; + if (this.hawking.timer) { + clearTimeout(this.hawking.timer); + this.hawking.timer = null; + } + if (this.hawking.warningTimer) { + clearTimeout(this.hawking.warningTimer); + this.hawking.warningTimer = null; + } + if (this.hawking.attackTimer) { + clearTimeout(this.hawking.attackTimer); + this.hawking.attackTimer = null; + } + this.hideHawkingWarning(); + + // 清除所有角色显示 + const characterOverlay = document.getElementById('character-overlay'); + if (characterOverlay) { + characterOverlay.innerHTML = ''; + } + + console.log('EnemyAI reset complete'); + } + + // 启动霍金机制 + startHawking() { + this.hawking.active = true; + this.hawking.warningLevel = 0; + console.log('Hawking started at cam6'); + + // 根据AI等级计算警告时间 + // AI等级越高,警告时间越短(更难) + // AI 1-5: 30秒 → 黄色 + // AI 6-10: 25秒 → 黄色 + // AI 11-15: 20秒 → 黄色 + // AI 16-20: 18秒 → 黄色 + let initialWarningTime = 30000; // 默认30秒 + + if (this.game.state.customNight && this.game.state.currentNight === 7) { + const hawkingLevel = this.game.state.customAILevels.hawking; + if (hawkingLevel >= 16) { + initialWarningTime = 18000; // 18秒 + } else if (hawkingLevel >= 11) { + initialWarningTime = 20000; // 20秒 + } else if (hawkingLevel >= 6) { + initialWarningTime = 25000; // 25秒 + } else { + initialWarningTime = 30000; // 30秒 + } + console.log(`Hawking AI Level ${hawkingLevel}: Initial warning in ${initialWarningTime/1000}s`); + } else { + // 普通夜晚使用默认20秒 + initialWarningTime = 20000; + } + + // X秒后开始黄色警告 + this.hawking.timer = setTimeout(() => { + this.showHawkingWarning('yellow'); + }, initialWarningTime); + } + + // 显示霍金警告 + showHawkingWarning(level) { + if (!this.hawking.active) return; + + console.log(`Hawking warning: ${level}`); + + // 根据AI等级计算警告升级时间 + // AI等级越高,警告升级越快(更难) + let yellowToRedTime = 5000; // 默认5秒 + let redToBreakTime = 5000; // 默认5秒 + + if (this.game.state.customNight && this.game.state.currentNight === 7) { + const hawkingLevel = this.game.state.customAILevels.hawking; + if (hawkingLevel >= 16) { + yellowToRedTime = 3000; // 3秒 + redToBreakTime = 3000; // 3秒 + } else if (hawkingLevel >= 11) { + yellowToRedTime = 4000; // 4秒 + redToBreakTime = 4000; // 4秒 + } else if (hawkingLevel >= 6) { + yellowToRedTime = 5000; // 5秒 + redToBreakTime = 5000; // 5秒 + } else { + yellowToRedTime = 6000; // 6秒 + redToBreakTime = 6000; // 6秒 + } + } + + if (level === 'yellow') { + this.hawking.warningLevel = 1; + this.updateHawkingWarningDisplay(); + + // X秒后升级为红色警告 + this.hawking.warningTimer = setTimeout(() => { + this.showHawkingWarning('red'); + }, yellowToRedTime); + } else if (level === 'red') { + this.hawking.warningLevel = 2; + this.updateHawkingWarningDisplay(); + + // X秒后摄像头坏掉 + this.hawking.warningTimer = setTimeout(() => { + this.hawkingBreakCamera(); + }, redToBreakTime); + } + } + + // 更新霍金警告显示 + updateHawkingWarningDisplay() { + let warningIcon = document.getElementById('hawking-warning-icon'); + + if (!warningIcon) { + warningIcon = document.createElement('img'); + warningIcon.id = 'hawking-warning-icon'; + warningIcon.style.position = 'absolute'; + warningIcon.style.zIndex = '1000'; + warningIcon.style.display = 'block'; + warningIcon.style.animation = 'flash 0.5s infinite'; + + // 根据摄像头状态决定添加到哪里 + if (this.game.state.cameraOpen) { + const cameraGrid = document.getElementById('camera-grid'); + if (cameraGrid) { + cameraGrid.appendChild(warningIcon); + } + } else { + document.body.appendChild(warningIcon); + } + } + + // 根据摄像头状态决定位置和大小 + if (this.game.state.cameraOpen) { + // 摄像头打开:在地图上 cam6 右边,使用相对于地图的百分比定位 + // cam6 位置: x: 77.2%, y: 82.2%, width: 13.0%, height: 8.0% + // 警告图标放在 cam6 右边 + warningIcon.style.position = 'absolute'; + warningIcon.style.left = '91%'; // 77.2% + 13.0% + 小间距 + warningIcon.style.top = '82.2%'; + warningIcon.style.width = '11.2%'; // 8% * 1.4 = 11.2% + warningIcon.style.height = 'auto'; + warningIcon.style.transform = 'none'; + + // 确保在地图容器中 + const cameraGrid = document.getElementById('camera-grid'); + if (cameraGrid && warningIcon.parentElement !== cameraGrid) { + cameraGrid.appendChild(warningIcon); + } + } else { + // 摄像头关闭:在风扇标志(氧气显示)左边,使用 fixed 定位 + warningIcon.style.position = 'fixed'; + warningIcon.style.left = 'auto'; + warningIcon.style.right = 'calc(2vw + 15vw)'; // 氧气显示右边距 + 氧气显示宽度 + 间距 + warningIcon.style.top = 'auto'; + warningIcon.style.bottom = '2vh'; + warningIcon.style.width = '3vw'; + warningIcon.style.height = 'auto'; + warningIcon.style.transform = 'none'; + + // 确保在 body 中 + if (warningIcon.parentElement !== document.body) { + document.body.appendChild(warningIcon); + } + } + + // 设置警告图片 + if (this.hawking.warningLevel === 1) { + warningIcon.src = '/assets/images/Warninglight.png'; + } else if (this.hawking.warningLevel === 2) { + warningIcon.src = '/assets/images/Warningheavy.png'; + } + } + + // 隐藏霍金警告 + hideHawkingWarning() { + const warningIcon = document.getElementById('hawking-warning-icon'); + if (warningIcon) { + warningIcon.remove(); + } + } + + // 霍金破坏摄像头 + hawkingBreakCamera() { + console.log('Hawking broke the camera!'); + + // 隐藏警告 + this.hideHawkingWarning(); + + // 摄像头坏掉 + this.triggerCameraFailure(); + + // 霍金从cam6消失 + this.hawking.active = false; + this.updateCameraDisplay(); + + // 4秒后跳杀 + this.hawking.attackTimer = setTimeout(() => { + this.triggerJumpscare('hawking'); + }, 4000); + } + + // 霍金的导弹跳杀动画 + triggerHawkingMissileJumpscare() { + console.log('Hawking missile jumpscare!'); + + // 停止所有音效 + this.game.assets.stopSound('vents'); + this.game.assets.stopSound('static'); + + // 创建跳杀动画容器 + const jumpscareContainer = document.createElement('div'); + jumpscareContainer.id = 'jumpscare-container'; + jumpscareContainer.style.position = 'fixed'; + jumpscareContainer.style.top = '0'; + jumpscareContainer.style.left = '0'; + jumpscareContainer.style.width = '100%'; + jumpscareContainer.style.height = '100%'; + jumpscareContainer.style.zIndex = '99999'; + jumpscareContainer.style.overflow = 'hidden'; + jumpscareContainer.style.backgroundColor = '#000'; + + // 创建办公室背景 + const officeBackground = document.createElement('img'); + officeBackground.src = this.game.assets.images.office.src; + officeBackground.style.position = 'absolute'; + officeBackground.style.top = '0'; + officeBackground.style.left = '0'; + officeBackground.style.width = '100%'; + officeBackground.style.height = '100%'; + officeBackground.style.objectFit = 'cover'; + officeBackground.style.zIndex = '1'; + + // 创建霍金图片(在房间里) + const hawkingImg = document.createElement('img'); + hawkingImg.src = '/assets/images/mrstephen.png'; + hawkingImg.style.position = 'absolute'; + hawkingImg.style.left = '43.6%'; + hawkingImg.style.bottom = '27.4%'; + hawkingImg.style.width = '30%'; + hawkingImg.style.height = 'auto'; + hawkingImg.style.zIndex = '2'; + hawkingImg.style.filter = 'brightness(0.68) contrast(1) saturate(1)'; + + // 创建导弹图片(从霍金位置飞向玩家) + const missileImg = document.createElement('img'); + missileImg.src = '/assets/images/front.png'; + missileImg.style.position = 'absolute'; + missileImg.style.left = '25%'; + missileImg.style.top = '40%'; + missileImg.style.width = '5%'; + missileImg.style.height = 'auto'; + missileImg.style.zIndex = '3'; + missileImg.style.transition = 'all 1s ease-out'; + + // 创建爆炸帧图容器(初始隐藏) + const explosionImg = document.createElement('div'); + explosionImg.style.position = 'absolute'; + explosionImg.style.top = '50%'; + explosionImg.style.left = '50%'; + explosionImg.style.transform = 'translate(-50%, -50%)'; + explosionImg.style.width = '50vw'; // 容器宽度 + explosionImg.style.height = '50vh'; // 容器高度 + explosionImg.style.zIndex = '4'; + explosionImg.style.backgroundImage = 'url(/assets/images/exp2.png)'; + explosionImg.style.backgroundSize = '400% auto'; // 4列,高度自适应 + explosionImg.style.backgroundRepeat = 'no-repeat'; + explosionImg.style.backgroundPosition = '0% 0%'; + explosionImg.style.display = 'none'; + + jumpscareContainer.appendChild(officeBackground); + jumpscareContainer.appendChild(hawkingImg); + jumpscareContainer.appendChild(missileImg); + jumpscareContainer.appendChild(explosionImg); + document.body.appendChild(jumpscareContainer); + + // 播放霍金跳杀音效 + this.game.assets.playSound('hawkingJumpscare', false, 1.0); + + // 导弹飞向玩家(放大并移动到中心) + setTimeout(() => { + missileImg.style.left = '50%'; + missileImg.style.top = '50%'; + missileImg.style.transform = 'translate(-50%, -50%)'; + missileImg.style.width = '80%'; + }, 50); + + // 1秒后导弹到达,开始爆炸动画 + setTimeout(() => { + missileImg.style.display = 'none'; + explosionImg.style.display = 'block'; + + // 播放爆炸帧动画(使用第一列的4帧,原图是4列3行) + let frame = 0; + const totalFrames = 3; // 0-3共4帧 + const frameInterval = 80; // 每帧80ms + + // 第一列的4帧对应的Y位置(手动调整后的精确位置) + const yPositions = [8.00, 36.50, 65.00, 93.00]; + + const animateExplosion = setInterval(() => { + // X位置固定在0%(第一列),Y位置使用预设数组 + explosionImg.style.backgroundPosition = `0% ${yPositions[frame]}%`; + + frame++; + if (frame > totalFrames) { + clearInterval(animateExplosion); + + // 爆炸动画结束后淡出 + setTimeout(() => { + jumpscareContainer.style.transition = 'opacity 0.5s'; + jumpscareContainer.style.opacity = '0'; + + setTimeout(() => { + document.body.removeChild(jumpscareContainer); + this.game.gameOver('GAME OVER'); + }, 500); + }, 200); + } + }, frameInterval); + }, 1000); + } + + // 电击霍金(玩家点击按钮) + shockHawking() { + if (!this.hawking.active) { + console.log('Hawking is not active'); + return false; + } + + console.log('Hawking shocked! Resetting timer...'); + + // 清除所有计时器 + if (this.hawking.timer) { + clearTimeout(this.hawking.timer); + this.hawking.timer = null; + } + if (this.hawking.warningTimer) { + clearTimeout(this.hawking.warningTimer); + this.hawking.warningTimer = null; + } + if (this.hawking.attackTimer) { + clearTimeout(this.hawking.attackTimer); + this.hawking.attackTimer = null; + } + + // 重置警告等级 + this.hawking.warningLevel = 0; + this.hideHawkingWarning(); + + // 播放电击音效 + this.game.assets.playSound('hawking_shock', false, 0.8); + + // 根据AI等级计算重置后的警告时间 + let resetWarningTime = 20000; // 默认20秒 + + if (this.game.state.customNight && this.game.state.currentNight === 7) { + const hawkingLevel = this.game.state.customAILevels.hawking; + if (hawkingLevel >= 16) { + resetWarningTime = 18000; // 18秒 + } else if (hawkingLevel >= 11) { + resetWarningTime = 20000; // 20秒 + } else if (hawkingLevel >= 6) { + resetWarningTime = 25000; // 25秒 + } else { + resetWarningTime = 30000; // 30秒 + } + } + + // X秒后重新开始警告 + this.hawking.timer = setTimeout(() => { + this.showHawkingWarning('yellow'); + }, resetWarningTime); + + return true; + } +} diff --git a/games/five-nights-at-epsteins/js/Game.js b/games/five-nights-at-epsteins/js/Game.js new file mode 100644 index 00000000..9bbf52bb --- /dev/null +++ b/games/five-nights-at-epsteins/js/Game.js @@ -0,0 +1,1432 @@ +// Main game class +class Game { + constructor() { + this.state = new GameState(); + this.assets = new AssetManager(); + this.ui = new UIManager(this); + this.camera = new CameraSystem(this); + this.enemyAI = new EnemyAI(this); + this.input = new InputHandler(this); + + // Initialize CameraSystem's EP config (from EnemyAI) + this.camera.initEPConfig(); + + this.timeInterval = null; + this.powerInterval = null; + this.viewPosition = 0.25; + this.isRotatingLeft = false; + this.isRotatingRight = false; + this.rotationSpeed = 0.015; + + this.initElements(); + this.bindEvents(); + } + + initElements() { + this.mainMenu = document.getElementById('main-menu'); + this.gameScreen = document.getElementById('game-screen'); + this.gameOverElement = document.getElementById('game-over'); + this.gameOverText = document.getElementById('game-over-text'); + this.tutorialOverlay = document.getElementById('tutorial-overlay'); + this.tutorialGotItBtn = document.getElementById('tutorial-got-it'); + + this.startBtn = document.getElementById('start-game'); + this.continueBtn = document.getElementById('continue-game'); + this.specialNightBtn = document.getElementById('special-night-btn'); + this.customNightBtn = document.getElementById('custom-night-btn'); + this.starIcon = document.getElementById('star-icon'); + this.starIcon2 = document.getElementById('star-icon-2'); + this.starIcon3 = document.getElementById('star-icon-3'); + this.restartBtn = document.getElementById('restart'); + this.mainMenuBtn = document.getElementById('main-menu-btn'); + + // 音量控制元素 + this.volumeBtn = document.getElementById('volume-btn'); + this.volumePanel = document.getElementById('volume-panel'); + this.closeVolumePanelBtn = document.getElementById('close-volume-panel'); + this.gameBgVolumeSlider = document.getElementById('game-bg-volume'); + this.menuMusicVolumeSlider = document.getElementById('menu-music-volume'); + this.jumpscareVolumeSlider = document.getElementById('jumpscare-volume'); + this.ventCrawlingVolumeSlider = document.getElementById('vent-crawling-volume'); + this.masterVolumeSlider = document.getElementById('master-volume'); + + // 调试:检查元素是否找到 + if (!this.volumeBtn) console.error('Volume button not found!'); + if (!this.volumePanel) console.error('Volume panel not found!'); + + // Custom Night 元素 + this.customNightMenu = document.getElementById('custom-night-menu'); + this.epsteinSlider = document.getElementById('epstein-slider'); + this.trumpSlider = document.getElementById('trump-slider'); + this.hawkingSlider = document.getElementById('hawking-slider'); + this.epsteinValue = document.getElementById('epstein-value'); + this.trumpValue = document.getElementById('trump-value'); + this.hawkingValue = document.getElementById('hawking-value'); + this.startCustomNightBtn = document.getElementById('start-custom-night'); + this.backToMenuBtn = document.getElementById('back-to-menu'); + + // 初始化音量设置 + this.initVolumeSettings(); + } + + initVolumeSettings() { + const volumes = this.assets.getAllVolumes(); + this.gameBgVolumeSlider.value = Math.round(volumes.gameBg * 100); + this.menuMusicVolumeSlider.value = Math.round(volumes.menuMusic * 100); + this.jumpscareVolumeSlider.value = Math.round(volumes.jumpscare * 100); + this.ventCrawlingVolumeSlider.value = Math.round(volumes.ventCrawling * 100); + this.masterVolumeSlider.value = Math.round(volumes.master * 100); + + // 更新百分比显示 + this.updateVolumePercents(); + } + + updateVolumePercents() { + const sliders = [ + this.gameBgVolumeSlider, + this.menuMusicVolumeSlider, + this.jumpscareVolumeSlider, + this.ventCrawlingVolumeSlider, + this.masterVolumeSlider + ]; + + sliders.forEach(slider => { + const percent = slider.parentElement.querySelector('.volume-percent'); + if (percent) { + percent.textContent = slider.value + '%'; + } + }); + } + + bindEvents() { + this.startBtn.addEventListener('click', () => this.startGame()); + this.continueBtn.addEventListener('click', () => this.continueGame()); + this.specialNightBtn.addEventListener('click', () => this.startSpecialNight()); + this.customNightBtn.addEventListener('click', () => this.showCustomNightMenu()); + this.restartBtn.addEventListener('click', () => this.restartGame()); + + // 音量面板事件 + this.volumeBtn.addEventListener('click', () => { + this.volumePanel.classList.toggle('hidden'); + }); + + this.closeVolumePanelBtn.addEventListener('click', () => { + this.volumePanel.classList.add('hidden'); + }); + + // 音量滑块事件 + this.gameBgVolumeSlider.addEventListener('input', (e) => { + this.assets.setVolume('gameBg', parseInt(e.target.value) / 100); + this.updateVolumePercents(); + // 立即更新游戏中的背景音效 + if (this.state.isGameRunning) { + const ventsSound = this.assets.sounds['vents']; + if (ventsSound && !ventsSound.paused) { + const volumes = this.assets.getAllVolumes(); + ventsSound.volume = volumes.gameBg * volumes.master; + } + } + }); + + this.menuMusicVolumeSlider.addEventListener('input', (e) => { + this.assets.setVolume('menuMusic', parseInt(e.target.value) / 100); + this.updateVolumePercents(); + // 立即更新主菜单音乐音量 + const menuMusic = document.getElementById('menu-music'); + if (menuMusic && !menuMusic.paused) { + const volumes = this.assets.getAllVolumes(); + menuMusic.volume = volumes.menuMusic * volumes.master; + } + }); + + this.jumpscareVolumeSlider.addEventListener('input', (e) => { + this.assets.setVolume('jumpscare', parseInt(e.target.value) / 100); + this.updateVolumePercents(); + }); + + this.ventCrawlingVolumeSlider.addEventListener('input', (e) => { + this.assets.setVolume('ventCrawling', parseInt(e.target.value) / 100); + this.updateVolumePercents(); + }); + + this.masterVolumeSlider.addEventListener('input', (e) => { + this.assets.setVolume('master', parseInt(e.target.value) / 100); + this.updateVolumePercents(); + // 立即更新所有正在播放的音效 + const menuMusic = document.getElementById('menu-music'); + if (menuMusic && !menuMusic.paused) { + const volumes = this.assets.getAllVolumes(); + menuMusic.volume = volumes.menuMusic * volumes.master; + } + if (this.state.isGameRunning) { + const ventsSound = this.assets.sounds['vents']; + if (ventsSound && !ventsSound.paused) { + const volumes = this.assets.getAllVolumes(); + ventsSound.volume = volumes.gameBg * volumes.master; + } + } + }); + this.mainMenuBtn.addEventListener('click', () => this.showMainMenu()); + this.tutorialGotItBtn.addEventListener('click', () => this.closeTutorial()); + + // Custom Night 事件 + this.startCustomNightBtn.addEventListener('click', () => this.startCustomNight()); + this.backToMenuBtn.addEventListener('click', () => this.hideCustomNightMenu()); + + // AI滑块事件 + this.epsteinSlider.addEventListener('input', (e) => { + this.epsteinValue.textContent = e.target.value; + }); + this.trumpSlider.addEventListener('input', (e) => { + this.trumpValue.textContent = e.target.value; + }); + this.hawkingSlider.addEventListener('input', (e) => { + this.hawkingValue.textContent = e.target.value; + }); + + // +/- 按钮事件 + document.querySelectorAll('.ai-btn-minus').forEach(btn => { + btn.addEventListener('click', () => { + const aiName = btn.dataset.ai; + const slider = document.getElementById(`${aiName}-slider`); + const value = Math.max(0, parseInt(slider.value) - 1); + slider.value = value; + document.getElementById(`${aiName}-value`).textContent = value; + }); + }); + + document.querySelectorAll('.ai-btn-plus').forEach(btn => { + btn.addEventListener('click', () => { + const aiName = btn.dataset.ai; + const slider = document.getElementById(`${aiName}-slider`); + const value = Math.min(20, parseInt(slider.value) + 1); + slider.value = value; + document.getElementById(`${aiName}-value`).textContent = value; + }); + }); + } + + // 加载保存的进度 + loadProgress() { + const savedNight = localStorage.getItem('fnae_current_night'); + if (savedNight) { + const night = parseInt(savedNight); + if (night > 1 && night <= this.state.maxNights) { + this.state.currentNight = night; + return true; + } + } + return false; + } + + // 保存进度 + saveProgress() { + if (this.state.currentNight > 1) { + localStorage.setItem('fnae_current_night', this.state.currentNight.toString()); + } + } + + // 清除进度 + clearProgress() { + localStorage.removeItem('fnae_current_night'); + } + + // 更新Continue按钮显示 + updateContinueButton() { + if (this.loadProgress()) { + this.continueBtn.classList.remove('hidden'); + this.continueBtn.textContent = `CONTINUE (NIGHT ${this.state.currentNight})`; + } else { + this.continueBtn.classList.add('hidden'); + } + + // 检查是否解锁特殊夜晚 + const night6Unlocked = localStorage.getItem('night6Unlocked'); + if (night6Unlocked === 'true') { + this.specialNightBtn.classList.remove('hidden'); + this.starIcon.classList.remove('hidden'); + } else { + this.specialNightBtn.classList.add('hidden'); + this.starIcon.classList.add('hidden'); + } + + // 检查是否通关Night 6 + const night6Completed = localStorage.getItem('night6Completed'); + if (night6Completed === 'true') { + this.starIcon2.classList.remove('hidden'); + this.customNightBtn.classList.remove('hidden'); // 通关Night 6后解锁Custom Night + } else { + this.starIcon2.classList.add('hidden'); + this.customNightBtn.classList.add('hidden'); + } + + // 检查是否通关20/20/20 Custom Night + const customNight202020 = localStorage.getItem('customNight202020'); + if (customNight202020 === 'true') { + this.starIcon3.classList.remove('hidden'); + } else { + this.starIcon3.classList.add('hidden'); + } + + // 恢复到Night 1(不影响按钮显示) + this.state.currentNight = 1; + } + + // 显示Custom Night菜单 + showCustomNightMenu() { + this.mainMenu.classList.add('hidden'); + this.customNightMenu.classList.remove('hidden'); + } + + // 隐藏Custom Night菜单 + hideCustomNightMenu() { + this.customNightMenu.classList.add('hidden'); + this.mainMenu.classList.remove('hidden'); + } + + // 开始Custom Night + async startCustomNight() { + const epsteinLevel = parseInt(this.epsteinSlider.value); + const trumpLevel = parseInt(this.trumpSlider.value); + const hawkingLevel = parseInt(this.hawkingSlider.value); + + // 保存自定义AI等级到state + this.state.customNight = true; + this.state.currentNight = 7; // Custom Night = Night 7 + this.state.customAILevels = { + epstein: epsteinLevel, + trump: trumpLevel, + hawking: hawkingLevel + }; + + console.log('Starting Custom Night with AI levels:', this.state.customAILevels); + + this.customNightMenu.classList.add('hidden'); + + // 隐藏音量按钮和面板 + if (this.volumeBtn) { + this.volumeBtn.classList.add('hidden'); + } + if (this.volumePanel) { + this.volumePanel.classList.add('hidden'); + } + + const menuMusic = document.getElementById('menu-music'); + if (menuMusic) { + menuMusic.pause(); + menuMusic.currentTime = 0; + menuMusic.loop = false; + } + + // 重置敌人AI状态 + this.enemyAI.reset(); + + // 直接开始游戏 + await this.initGame(); + } + + // Continue游戏(从保存的关卡开始) + async continueGame() { + if (this.loadProgress()) { + this.mainMenu.classList.add('hidden'); + + // 隐藏音量按钮和面板 + if (this.volumeBtn) { + this.volumeBtn.classList.add('hidden'); + } + if (this.volumePanel) { + this.volumePanel.classList.add('hidden'); + } + + const menuMusic = document.getElementById('menu-music'); + if (menuMusic) { + menuMusic.pause(); + menuMusic.currentTime = 0; + menuMusic.loop = false; + } + + // 重置敌人AI状态 + this.enemyAI.reset(); + + // 直接开始游戏,不播放过场动画 + await this.initGame(); + } + } + + // 开始特殊夜晚(Night 6) + async startSpecialNight() { + this.state.currentNight = 6; // 设置为Night 6 + this.clearProgress(); // 清除普通进度 + + this.mainMenu.classList.add('hidden'); + + // 隐藏音量按钮和面板 + if (this.volumeBtn) { + this.volumeBtn.classList.add('hidden'); + } + if (this.volumePanel) { + this.volumePanel.classList.add('hidden'); + } + + const menuMusic = document.getElementById('menu-music'); + if (menuMusic) { + menuMusic.pause(); + menuMusic.currentTime = 0; + menuMusic.loop = false; + } + + // 重置敌人AI状态 + this.enemyAI.reset(); + + // 直接开始游戏,不播放过场动画 + await this.initGame(); + } + + async startGame() { + // NEW GAME总是从Night 1开始 + this.state.currentNight = 1; + this.clearProgress(); // 清除之前的进度 + + this.mainMenu.classList.add('hidden'); + + // 隐藏音量按钮和面板 + if (this.volumeBtn) { + this.volumeBtn.classList.add('hidden'); + } + if (this.volumePanel) { + this.volumePanel.classList.add('hidden'); + } + + const menuMusic = document.getElementById('menu-music'); + if (menuMusic) { + menuMusic.pause(); + menuMusic.currentTime = 0; + menuMusic.loop = false; + } + + // 重置敌人AI状态 + this.enemyAI.reset(); + + const cutscene = document.getElementById('cutscene'); + cutscene.classList.remove('hidden'); + + // 触发淡入效果 + setTimeout(() => { + cutscene.classList.add('fade-in'); + }, 50); + + let cutsceneEnded = false; + + const endCutscene = () => { + if (cutsceneEnded) return; + cutsceneEnded = true; + + // 淡出效果 + cutscene.classList.remove('fade-in'); + cutscene.classList.add('fade-out'); + + // 等待淡出完成后隐藏并开始游戏 + setTimeout(() => { + cutscene.classList.add('hidden'); + cutscene.classList.remove('fade-out'); + // 不在这里显示游戏画面,让initGame处理 + this.initGame(); + }, 3000); + + cutscene.removeEventListener('click', endCutscene); + if (autoEndTimeout) clearTimeout(autoEndTimeout); + }; + + // 点击跳过 + cutscene.addEventListener('click', endCutscene); + + // 3秒后自动开始淡出(总共6秒:3秒淡入 + 3秒淡出) + const autoEndTimeout = setTimeout(() => { + endCutscene(); + }, 3000); + } + + async initGame() { + // console.log('🎮 initGame called, currentNight:', this.state.currentNight); + + if (!this.assets.loaded) { + await this.assets.loadAssets(); + } + + this.state.reset(); + + // console.log('🎮 After state.reset(), currentNight:', this.state.currentNight); + + // 重置摄像头系统的sound按钮计数 + this.camera.resetSoundButtonCount(); + + // 恢复摄像头面板的display(之前可能被强制隐藏) + const cameraPanel = document.getElementById('camera-panel'); + if (cameraPanel) { + cameraPanel.style.display = ''; // 恢复默认 + // console.log('🎮 Camera panel display restored'); + } + + // 显示每晚开始场景(在显示游戏画面之前) + await this.showNightIntro(); + + // console.log('🎮 After showNightIntro(), currentNight:', this.state.currentNight); + + // 进场动画结束后才显示游戏画面 + this.gameScreen.classList.add('active'); + + this.ui.currentSceneImg.src = this.assets.images.office.src; + this.ui.currentSceneImg.style.display = 'block'; + this.viewPosition = 0.25; + this.ui.updateViewPosition(this.viewPosition); + + this.ui.update(); + this.ui.createHotspots(); + + // 初始化风扇状态(通风口默认打开,风扇快速旋转) + this.initVentFanAnimation(); + + this.startGameLoop(); + this.startViewRotation(); + + // 前3个夜晚:等待指南关闭后再启动敌人 AI + // Night 4+:直接启动敌人 AI + if (this.state.currentNight > 3) { + this.enemyAI.start(); + } + + this.assets.playSound('vents', true); + + // Show tutorial + if (this.state.currentNight === 1) { + this.showTutorial('night1'); + } else if (this.state.currentNight === 2) { + this.showTutorial('night2'); + } else if (this.state.currentNight === 3) { + this.showTutorial('night3'); + } + + // console.log('🎮 Before Golden check, currentNight:', this.state.currentNight); + + // Night 5: 必定触发 Golden 霍金彩蛋(放在最后,确保游戏已完全初始化) + if (this.state.currentNight === 5) { + // console.log('🌟 Night 5 detected, triggering Golden Stephen...'); + setTimeout(() => { + this.showGoldenStephen(); + }, 1000); // 进入游戏1秒后触发 + } // else { + // console.log('❌ Not Night 5, currentNight is:', this.state.currentNight); + // } + } + + // 初始化风扇动画状态 + initVentFanAnimation() { + const ventIcon = document.querySelector('.vent-icon'); + if (ventIcon) { + if (this.state.ventsClosed) { + // 通风口关闭,风扇停止 + ventIcon.classList.add('stopped'); + ventIcon.style.animation = 'none'; + } else { + // 通风口打开,风扇快速旋转 + ventIcon.classList.remove('stopped', 'slowing', 'speeding-up'); + ventIcon.style.animation = 'spin-fast 0.333s linear infinite'; + } + } + } + + showTutorial(type = 'night1') { + const tutorialContent = document.getElementById('tutorial-content'); + if (!tutorialContent) return; + + if (type === 'night2') { + // Night 2 教程:Trump + tutorialContent.innerHTML = ` +

DEFEND YOURSELF AGAINST TRUMP

+

+ TRUMP WILL TRY TO ATTACK YOU THROUGH THE VENTS IN CAM 1 AND CAM 2, SO IF YOU HEAR BANGING IN THE VENTS HEAD OVER TO THE CONTROL PANEL AND CLOSE THEM. + AFTER CLOSING THEM YOU WILL HEAR BANGING AGAIN AFTER A FEW SECONDS WHICH MEANS HE LEFT THE VENTS. YOU MUST OPEN THE VENTS OTHERWISE YOU WILL DIE FROM LACK OF OXYGEN. + TRUMP CAN BE LURED WITH THE AUDIOS BUT YOUR MAIN PRIORITY WITH THE AUDIO LURES SHOULD BE EPSTEIN. +

+ + `; + // 重新绑定按钮事件 + const gotItBtn = document.getElementById('tutorial-got-it'); + if (gotItBtn) { + gotItBtn.addEventListener('click', () => this.closeTutorial()); + } + } else if (type === 'night3') { + // Night 3 教程:霍金 + tutorialContent.innerHTML = ` +

DEFEND YOURSELF AGAINST STEPHEN HAWKING

+

+ STEPHEN HAWKING ALWAYS STAYS AT CAM 6 AND HE IS NOT AFFECTED BY THE AUDIO LURES. + ELECTROCUTE STEPHEN HAWKING EVERY ONCE IN A WHILE TO PREVENT HIM FROM LEAVING CAM 6. +

+ + `; + // 重新绑定按钮事件 + const gotItBtn = document.getElementById('tutorial-got-it'); + if (gotItBtn) { + gotItBtn.addEventListener('click', () => this.closeTutorial()); + } + } else { + // Night 1 教程:EP + tutorialContent.innerHTML = ` +

DEFEND YOURSELF AGAINST EPSTEIN

+

+ EPSTEIN ALWAYS STARTS AT CAM 11. USE THE CAMERA'S AUDIO LURE TO KEEP EPSTEIN FAR AWAY FROM YOU. + MAKE SURE THE CAMERA YOU'RE PLAYING THE SOUND IN IS NEXT TO THE CAMERA WHERE EPSTEIN IS. + PLAYING SOUND IN ONLY ONE SPOT WILL NOT WORK IF YOU DO IT TWICE OR MORE IN A ROW. + USING THE AUDIO LURE TOO MUCH WILL LEAD TO THE CAMERAS BREAKING. + TO FIX THEM HEAD TO THE CONTROL PANEL AND RESTART THE CAMERAS LIKE YOU JUST DID. + EPSTEIN DOES NOT ATTACK THROUGH THE VENTS SO DON'T BOTHER CLOSING THEM FOR THIS NIGHT. +

+ + `; + // 重新绑定按钮事件 + const gotItBtn = document.getElementById('tutorial-got-it'); + if (gotItBtn) { + gotItBtn.addEventListener('click', () => this.closeTutorial()); + } + } + + this.tutorialOverlay.classList.remove('hidden'); + // Mark tutorial as active (but don't pause game, allow view rotation) + this.state.tutorialActive = true; + } + + closeTutorial() { + this.tutorialOverlay.classList.add('hidden'); + // Close tutorial + this.state.tutorialActive = false; + + // 前3个夜晚:关闭指南后启动敌人 AI + if (this.state.currentNight <= 3) { + console.log('🎮 Tutorial closed, starting enemy AI...'); + this.enemyAI.start(); + } + } + + // Golden 霍金彩蛋效果 + showGoldenStephen() { + console.log('🌟 Golden Stephen Hawking appears!'); + + // 创建全屏金色霍金图层 + const goldenOverlay = document.createElement('div'); + goldenOverlay.id = 'golden-stephen-overlay'; + goldenOverlay.style.position = 'fixed'; + goldenOverlay.style.top = '0'; + goldenOverlay.style.left = '0'; + goldenOverlay.style.width = '100%'; + goldenOverlay.style.height = '100%'; + goldenOverlay.style.zIndex = '9999'; + goldenOverlay.style.pointerEvents = 'none'; + goldenOverlay.style.background = 'rgba(0, 0, 0, 0.3)'; + + // 创建金色霍金图片 + const goldenImg = document.createElement('img'); + goldenImg.src = '/assets/images/goldenstephen.png'; + goldenImg.style.position = 'absolute'; + goldenImg.style.top = '50%'; + goldenImg.style.left = '50%'; + goldenImg.style.transform = 'translate(-50%, -50%)'; + goldenImg.style.width = '80%'; + goldenImg.style.height = '80%'; + goldenImg.style.objectFit = 'contain'; + goldenImg.style.opacity = '0'; + goldenImg.style.animation = 'golden-flicker 2s ease-in-out'; + + goldenOverlay.appendChild(goldenImg); + document.body.appendChild(goldenOverlay); + + // 播放音效 + this.assets.playSound('goldenstephenscare', false, 1.0); + + // 2秒后移除 + setTimeout(() => { + goldenOverlay.remove(); + }, 2000); + } + + showNightIntro() { + return new Promise((resolve) => { + const nightIntro = document.getElementById('night-intro'); + const nightIntroText = document.getElementById('night-intro-text'); + + // Update night number text + if (this.state.customNight && this.state.currentNight === 7) { + nightIntroText.textContent = 'CUSTOM NIGHT'; + } else { + nightIntroText.textContent = `NIGHT ${this.state.currentNight}`; + } + + // Show scene + nightIntro.classList.remove('hidden'); + + // Fade in effect (1.5s) + setTimeout(() => { + nightIntro.classList.add('fade-in'); + }, 50); + + // 1.5s fade in + 2s display then start fade out + setTimeout(() => { + nightIntro.classList.remove('fade-in'); + nightIntro.classList.add('fade-out'); + + // After 1.5s fade out complete, hide and continue game + setTimeout(() => { + nightIntro.classList.add('hidden'); + nightIntro.classList.remove('fade-out'); + resolve(); + }, 1500); + }, 3500); // 1500ms fade in + 2000ms display + }); + } + + + startViewRotation() { + const rotationLoop = () => { + if (!this.state.isGameRunning) return; + + // If control panel or camera is open, disable rotation + if (!this.state.controlPanelOpen && !this.state.cameraOpen) { + if (this.isRotatingLeft && this.viewPosition > 0) { + this.viewPosition -= this.rotationSpeed; + this.viewPosition = Math.max(0, this.viewPosition); + this.ui.updateViewPosition(this.viewPosition); + } + + if (this.isRotatingRight && this.viewPosition < 1) { + this.viewPosition += this.rotationSpeed; + this.viewPosition = Math.min(1, this.viewPosition); + this.ui.updateViewPosition(this.viewPosition); + } + } + + requestAnimationFrame(rotationLoop); + }; + + rotationLoop(); + } + + startGameLoop() { + this.timeInterval = setInterval(() => { + // 前3个夜晚:如果指南打开,暂停时间 + if (this.state.currentNight <= 3 && this.state.tutorialActive) { + return; // 跳过时间更新 + } + + this.state.currentTime += 1; + this.ui.update(); + + if (this.state.currentTime >= 6) { + this.winNight(); + } + }, 60000); + + this.powerInterval = setInterval(() => { + this.updatePower(); + }, 1000); + } + + updatePower() { + // 前3个夜晚:如果指南打开,暂停氧气消耗 + if (this.state.currentNight <= 3 && this.state.tutorialActive) { + return; // 跳过氧气更新 + } + + if (this.state.ventsClosed) { + // When vents closed, oxygen decreases (faster speed) + this.state.oxygen -= 1.5; + } else { + // When vents open, oxygen quickly recovers to 100% + if (this.state.oxygen < 100) { + this.state.oxygen += 2; + } + } + + this.state.oxygen = Math.max(0, Math.min(100, this.state.oxygen)); + + if (this.state.oxygen <= 0) { + this.oxygenOut(); + } + + this.ui.update(); + } + + toggleVents() { + console.log('toggleVents called, controlPanelBusy:', this.state.controlPanelBusy); + + // 如果控制面板正忙,不允许操作 + if (this.state.controlPanelBusy) { + console.log('Control panel is busy, please wait...'); + return; + } + + // 标记控制面板为忙碌状态 + this.state.controlPanelBusy = true; + this.state.ventsToggling = true; + console.log('Starting vent toggle animation...'); + + // 播放心电图音效 + this.assets.playSound('ekg', false, 0.8); + + // 获取风扇图标 + const ventIcon = document.querySelector('.vent-icon'); + + if (this.state.ventsClosed) { + // 当前关闭,要打开 -> 风扇从停止加速到快速 + console.log('Opening vents: fan speeding up'); + if (ventIcon) { + ventIcon.classList.remove('stopped', 'slowing'); + ventIcon.classList.add('speeding-up'); + + // 逐步加速动画 + setTimeout(() => { + ventIcon.style.animation = 'spin-slow 2s linear infinite'; + }, 0); + setTimeout(() => { + ventIcon.style.animation = 'spin-slow 1.5s linear infinite'; + }, 1000); + setTimeout(() => { + ventIcon.style.animation = 'spin-fast 0.333s linear infinite'; + ventIcon.classList.remove('speeding-up'); + }, 2000); + } + } else { + // 当前打开,要关闭 -> 风扇从快速减速到停止 + console.log('Closing vents: fan slowing down'); + if (ventIcon) { + ventIcon.classList.remove('speeding-up'); + ventIcon.classList.add('slowing'); + + // 逐步减速动画 + setTimeout(() => { + ventIcon.style.animation = 'spin-slow 1.5s linear infinite'; + }, 0); + setTimeout(() => { + ventIcon.style.animation = 'spin-slow 2s linear infinite'; + }, 1000); + setTimeout(() => { + ventIcon.style.animation = 'spin-slow 3s linear infinite'; + }, 2000); + setTimeout(() => { + ventIcon.style.animation = 'none'; + ventIcon.classList.remove('slowing'); + ventIcon.classList.add('stopped'); + }, 3000); + } + } + + // 更新UI显示点动画 + this.ui.updateVentsStatus(); + + // 启动定时更新(每100ms更新一次UI) + const updateInterval = setInterval(() => { + this.ui.updateVentsStatus(); + if (!this.state.ventsToggling) { + clearInterval(updateInterval); + } + }, 100); + + // 4秒后完成切换 + setTimeout(() => { + this.state.ventsClosed = !this.state.ventsClosed; + console.log('Vents:', this.state.ventsClosed ? 'closed' : 'open'); + + // 通知 EnemyAI 通风口状态变化 + this.enemyAI.onVentsChanged(this.state.ventsClosed); + + // 解除锁定 + this.state.ventsToggling = false; + this.state.controlPanelBusy = false; + console.log('Vent toggle completed'); + + // 更新UI和控制面板选项文本 + this.ui.update(); + this.ui.updateVentsStatus(); + this.ui.updateControlPanelOptions(); + }, 4000); + } + + toggleCamera() { + // console.log('🎮 Game.toggleCamera() called'); + // console.log('🎮 Current state - cameraOpen:', this.state.cameraOpen, 'tutorialActive:', this.state.tutorialActive); + this.camera.toggle(); + } + + oxygenOut() { + this.stopGame(); + this.assets.stopSound('ambient'); + // Oxygen depleted triggers jumpscare + this.enemyAI.triggerJumpscare(); + } + + gameOver(message) { + this.stopGame(); + this.assets.stopSound('ambient'); + + // 立即隐藏游戏画面 + this.gameScreen.classList.remove('active'); + + // 关闭摄像头面板 + if (this.state.cameraOpen) { + this.camera.close(); + } + + // 隐藏摄像头面板 + const cameraPanel = document.getElementById('camera-panel'); + if (cameraPanel) { + cameraPanel.classList.add('hidden'); + cameraPanel.classList.remove('show'); + } + + // 清理角色图层 + const characterOverlay = document.getElementById('character-overlay'); + if (characterOverlay) { + characterOverlay.innerHTML = ''; + } + + // 隐藏控制面板 + const controlPanel = document.getElementById('control-panel'); + if (controlPanel) { + controlPanel.classList.add('hidden'); + } + + this.gameOverScreen(message); + } + + winNight() { + this.stopGame(); + this.assets.stopSound('ambient'); + + // 关闭摄像头(如果打开) + if (this.state.cameraOpen) { + this.camera.close(); + } + + // 强制隐藏摄像头面板,防止闪现 + const cameraPanel = document.getElementById('camera-panel'); + if (cameraPanel) { + cameraPanel.classList.add('hidden'); + cameraPanel.classList.remove('show', 'closing'); + cameraPanel.style.display = 'none'; // 强制隐藏 + } + + // 立即隐藏游戏画面,防止闪烁 + this.gameScreen.classList.remove('active'); + + // 检查是否通关20/20/20 Custom Night + if (this.state.customNight && this.state.currentNight === 7) { + const levels = this.state.customAILevels; + if (levels.epstein === 20 && levels.trump === 20 && levels.hawking === 20) { + console.log('🌟 20/20/20 Custom Night completed!'); + localStorage.setItem('customNight202020', 'true'); + } + } + + // 如果是 Night 6,播放特殊的胜利动画并标记完成 + if (this.state.currentNight === 6) { + localStorage.setItem('night6Completed', 'true'); + this.playNight6VictoryAnimation(); + } else if (this.state.currentNight === 5) { + // Night 5,播放特殊的胜利动画 + this.playNight5VictoryAnimation(); + } else { + // 其他关卡播放普通的夜晚结束动画 + this.playNightEndAnimation(); + } + } + + // Night 5 特殊胜利动画 + playNight5VictoryAnimation() { + // 创建全屏动画容器 + const animationContainer = document.createElement('div'); + animationContainer.style.position = 'fixed'; + animationContainer.style.top = '0'; + animationContainer.style.left = '0'; + animationContainer.style.width = '100%'; + animationContainer.style.height = '100%'; + animationContainer.style.backgroundColor = '#000'; + animationContainer.style.display = 'flex'; + animationContainer.style.alignItems = 'center'; + animationContainer.style.justifyContent = 'center'; + animationContainer.style.zIndex = '10000'; + animationContainer.style.opacity = '0'; + animationContainer.style.transition = 'opacity 0.5s'; + + // 创建时间显示 + const timeDisplay = document.createElement('div'); + timeDisplay.style.fontSize = '10vw'; + timeDisplay.style.fontWeight = 'bold'; + timeDisplay.style.color = '#fff'; + timeDisplay.style.fontFamily = 'Arial, sans-serif'; + timeDisplay.textContent = '5:59 AM'; + + animationContainer.appendChild(timeDisplay); + document.body.appendChild(animationContainer); + + // 淡入 + setTimeout(() => { + animationContainer.style.opacity = '1'; + }, 50); + + // 1秒后变为 6:00 AM 并播放钟声 + setTimeout(() => { + timeDisplay.textContent = '6:00 AM'; + this.assets.playSound('chimes', false, 1.0); + }, 1000); + + // 3秒后淡出时间 + setTimeout(() => { + timeDisplay.style.transition = 'opacity 0.5s'; + timeDisplay.style.opacity = '0'; + + setTimeout(() => { + // 移除时间显示 + animationContainer.removeChild(timeDisplay); + + // 创建 "RESCUE ARRIVE" 文字 + const rescueText = document.createElement('div'); + rescueText.style.fontSize = '8vw'; + rescueText.style.fontWeight = 'bold'; + rescueText.style.color = '#0f0'; // 绿色,表示救援 + rescueText.style.fontFamily = 'Arial, sans-serif'; + rescueText.style.textAlign = 'center'; + rescueText.style.opacity = '0'; + rescueText.style.transition = 'opacity 1s'; + rescueText.textContent = 'RESCUE ARRIVE'; + + animationContainer.appendChild(rescueText); + + // 淡入 "RESCUE ARRIVE" + setTimeout(() => { + rescueText.style.opacity = '1'; + }, 50); + + // 2秒后淡出 "RESCUE ARRIVE",显示胜利画面 + setTimeout(() => { + rescueText.style.opacity = '0'; + + setTimeout(() => { + // 移除 "RESCUE ARRIVE" 文字 + animationContainer.removeChild(rescueText); + + // 创建胜利画面 + const winScreen = document.createElement('img'); + winScreen.src = '/assets/images/winscreen.png'; + winScreen.style.width = '100%'; + winScreen.style.height = '100%'; + winScreen.style.objectFit = 'contain'; + winScreen.style.opacity = '0'; + winScreen.style.transition = 'opacity 1s'; + + animationContainer.appendChild(winScreen); + + // 播放胜利音乐 + this.assets.playSound('win', false, 1.0); + + // 淡入胜利画面 + setTimeout(() => { + winScreen.style.opacity = '1'; + }, 50); + + // 5秒后淡出并返回主菜单 + setTimeout(() => { + animationContainer.style.opacity = '0'; + + setTimeout(() => { + document.body.removeChild(animationContainer); + + // Night 5 通关后,解锁 Night 6(Special Night) + localStorage.setItem('night6Unlocked', 'true'); + + // 返回主菜单 + this.clearProgress(); + this.showMainMenu(); + }, 500); + }, 5000); // 显示5秒 + }, 1000); // "RESCUE ARRIVE" 淡出1秒 + }, 2000); // 显示 "RESCUE ARRIVE" 2秒 + }, 500); // 时间淡出0.5秒 + }, 3000); // 显示 "6:00 AM" 2秒 + } + + // Night 6 特殊胜利动画 + playNight6VictoryAnimation() { + // 创建全屏动画容器 + const animationContainer = document.createElement('div'); + animationContainer.style.position = 'fixed'; + animationContainer.style.top = '0'; + animationContainer.style.left = '0'; + animationContainer.style.width = '100%'; + animationContainer.style.height = '100%'; + animationContainer.style.backgroundColor = '#000'; + animationContainer.style.display = 'flex'; + animationContainer.style.alignItems = 'center'; + animationContainer.style.justifyContent = 'center'; + animationContainer.style.zIndex = '10000'; + animationContainer.style.opacity = '0'; + animationContainer.style.transition = 'opacity 0.5s'; + + // 创建时间显示 + const timeDisplay = document.createElement('div'); + timeDisplay.style.fontSize = '10vw'; + timeDisplay.style.fontWeight = 'bold'; + timeDisplay.style.color = '#fff'; + timeDisplay.style.fontFamily = 'Arial, sans-serif'; + timeDisplay.textContent = '5:59 AM'; + + animationContainer.appendChild(timeDisplay); + document.body.appendChild(animationContainer); + + // 淡入 + setTimeout(() => { + animationContainer.style.opacity = '1'; + }, 50); + + // 1秒后变为 6:00 AM 并播放钟声 + setTimeout(() => { + timeDisplay.textContent = '6:00 AM'; + this.assets.playSound('chimes', false, 1.0); + }, 1000); + + // 3秒后淡出时间,显示night6.png图片 + setTimeout(() => { + timeDisplay.style.transition = 'opacity 0.5s'; + timeDisplay.style.opacity = '0'; + + setTimeout(() => { + // 移除时间显示 + animationContainer.removeChild(timeDisplay); + + // 创建night6.png图片 + const night6Image = document.createElement('img'); + night6Image.src = '/assets/images/night6.png'; + night6Image.style.width = '100%'; + night6Image.style.height = '100%'; + night6Image.style.objectFit = 'contain'; + night6Image.style.opacity = '0'; + night6Image.style.transition = 'opacity 1s'; + + animationContainer.appendChild(night6Image); + + // 播放goldenstephenscare.ogg音乐 + this.assets.playSound('goldenstephenscare', false, 1.0); + + // 淡入图片 + setTimeout(() => { + night6Image.style.opacity = '1'; + }, 50); + + // 5秒后淡出并返回主菜单 + setTimeout(() => { + animationContainer.style.opacity = '0'; + + setTimeout(() => { + document.body.removeChild(animationContainer); + + // 返回主菜单 + this.showMainMenu(); + }, 500); + }, 5000); + }, 500); + }, 3000); + } + + // Night end animation: 5:59AM -> 6:00AM -> Days until rescue + playNightEndAnimation() { + // Create fullscreen animation container + const animationContainer = document.createElement('div'); + animationContainer.style.position = 'fixed'; + animationContainer.style.top = '0'; + animationContainer.style.left = '0'; + animationContainer.style.width = '100%'; + animationContainer.style.height = '100%'; + animationContainer.style.backgroundColor = '#000'; + animationContainer.style.display = 'flex'; + animationContainer.style.alignItems = 'center'; + animationContainer.style.justifyContent = 'center'; + animationContainer.style.zIndex = '10000'; + animationContainer.style.opacity = '0'; + animationContainer.style.transition = 'opacity 0.5s'; + + // 创建时间显示 + const timeDisplay = document.createElement('div'); + timeDisplay.style.fontSize = '10vw'; + timeDisplay.style.fontWeight = 'bold'; + timeDisplay.style.color = '#fff'; + timeDisplay.style.fontFamily = 'Arial, sans-serif'; + timeDisplay.textContent = '5:59 AM'; + + animationContainer.appendChild(timeDisplay); + document.body.appendChild(animationContainer); + + // Fade in + setTimeout(() => { + animationContainer.style.opacity = '1'; + }, 50); + + // After 1s change to 6:00AM and play sound effect + setTimeout(() => { + timeDisplay.textContent = '6:00 AM'; + this.assets.playSound('chimes', false, 1.0); + }, 1000); + + // After 2s more, show message + setTimeout(() => { + // 淡出时间(不改变容器透明度,保持黑色背景) + timeDisplay.style.transition = 'opacity 0.5s'; + timeDisplay.style.opacity = '0'; + + setTimeout(() => { + // Custom Night 通关显示 + if (this.state.customNight && this.state.currentNight === 7) { + timeDisplay.textContent = 'CUSTOM NIGHT COMPLETE'; + timeDisplay.style.fontSize = '5vw'; + timeDisplay.style.color = '#0f0'; // 绿色表示完成 + } + // 如果还有下一关,显示剩余天数(故事设定是5晚,所以总是显示5-当前关卡) + else if (this.state.currentNight < this.state.maxNights) { + const daysRemaining = 5 - this.state.currentNight; // 固定按5晚计算 + timeDisplay.textContent = `${daysRemaining} ${daysRemaining === 1 ? 'day' : 'days'} until rescue`; + timeDisplay.style.fontSize = '5vw'; + timeDisplay.style.color = '#fff'; + } else { + // 所有关卡完成,显示TO BE CONTINUED + timeDisplay.innerHTML = 'TO BE CONTINUED...
Web version port in progress'; + timeDisplay.style.fontSize = '5vw'; + timeDisplay.style.color = '#fff'; + } + timeDisplay.style.opacity = '1'; + }, 500); + + // 再过3秒后淡出 + setTimeout(() => { + animationContainer.style.opacity = '0'; + + setTimeout(() => { + document.body.removeChild(animationContainer); + + // Custom Night 通关后返回主菜单 + if (this.state.customNight && this.state.currentNight === 7) { + this.showMainMenu(); + } + // 如果还有下一关,直接进入下一关 + else if (this.state.currentNight < this.state.maxNights) { + this.state.currentNight++; + this.continueToNextNight(); + } else { + // 所有关卡完成,清除进度并返回主菜单 + this.clearProgress(); + this.showMainMenu(); + } + }, 500); + }, 3000); // 改为3秒,让玩家有时间看消息 + }, 3000); + } + + gameOverScreen(message, win = false) { + this.gameOverText.textContent = message; + const subtitle = document.getElementById('game-over-subtitle'); + const gameOverStatic = document.getElementById('game-over-static'); + const restartBtn = document.getElementById('restart'); + const mainMenuBtn = document.getElementById('main-menu-btn'); + + // 隐藏按钮 + if (restartBtn) restartBtn.style.display = 'none'; + if (mainMenuBtn) mainMenuBtn.style.display = 'none'; + + // Play static video + if (gameOverStatic) { + gameOverStatic.currentTime = 0; + gameOverStatic.play().catch(e => console.log('Failed to play game over static:', e)); + } + + if (win) { + // Only increase night number if not at max level + if (this.state.currentNight < this.state.maxNights) { + this.state.currentNight++; + // Hide subtitle, will continue to next night + subtitle.classList.add('hidden'); + + // Show game over screen + this.gameOverElement.classList.remove('hidden'); + + // Auto continue to next night after 3 seconds + setTimeout(() => { + // 隐藏游戏结束画面 + this.gameOverElement.classList.add('hidden'); + this.gameScreen.classList.remove('active'); + + // 停止静态视频 + if (gameOverStatic) { + gameOverStatic.pause(); + gameOverStatic.currentTime = 0; + } + + this.continueToNextNight(); + }, 3000); + } else { + // All available levels completed, show "to be continued" message + subtitle.textContent = 'TO BE CONTINUED... (Web version port in progress)'; + subtitle.classList.remove('hidden'); + this.gameOverElement.classList.remove('hidden'); + + // 3秒后自动返回主菜单 + setTimeout(() => { + this.gameOverElement.classList.add('hidden'); + this.showMainMenu(); + }, 3000); + } + } else { + // On failure hide subtitle + subtitle.classList.add('hidden'); + this.gameOverElement.classList.remove('hidden'); + + // 保存进度(如果在Night 2或更高关卡死亡) + this.saveProgress(); + + // 3秒后自动返回主菜单 + setTimeout(() => { + this.gameOverElement.classList.add('hidden'); + this.showMainMenu(); + }, 3000); + } + } + + // Continue to next night (without cutscene) + async continueToNextNight() { + if (!this.assets.loaded) { + await this.assets.loadAssets(); + } + + this.state.reset(); + this.enemyAI.reset(); // 重置敌人AI状态 + + // 重置摄像头系统的sound按钮计数 + this.camera.resetSoundButtonCount(); + + // 恢复摄像头面板的display(之前被强制隐藏) + const cameraPanel = document.getElementById('camera-panel'); + if (cameraPanel) { + cameraPanel.style.display = ''; // 恢复默认 + } + + // 显示每晚开始场景 + await this.showNightIntro(); + + // 进场动画结束后才显示游戏画面 + this.gameScreen.classList.add('active'); + + this.ui.currentSceneImg.src = this.assets.images.office.src; + this.ui.currentSceneImg.style.display = 'block'; + this.viewPosition = 0.25; + this.ui.updateViewPosition(this.viewPosition); + + this.ui.update(); + this.ui.createHotspots(); + + // 初始化风扇状态 + this.initVentFanAnimation(); + + this.startGameLoop(); + this.startViewRotation(); + + // 前3个夜晚:等待指南关闭后再启动敌人 AI + // Night 4+:直接启动敌人 AI + if (this.state.currentNight > 3) { + this.enemyAI.start(); + } + + this.assets.playSound('vents', true); + + // Show tutorial for specific nights + if (this.state.currentNight === 2) { + this.showTutorial('night2'); + } else if (this.state.currentNight === 3) { + this.showTutorial('night3'); + } + + // Night 5: 必定触发 Golden 霍金彩蛋 + if (this.state.currentNight === 5) { + console.log('🌟 Night 5 detected (continueToNextNight), triggering Golden Stephen...'); + setTimeout(() => { + this.showGoldenStephen(); + }, 1000); + } + } + + stopGame() { + this.state.isGameRunning = false; + clearInterval(this.timeInterval); + clearInterval(this.powerInterval); + this.enemyAI.stop(); + } + + restartGame() { + this.gameOverElement.classList.add('hidden'); + // Hide game screen, prepare to restart + this.gameScreen.classList.remove('active'); + + // 如果是Custom Night,重新开始Custom Night + if (this.state.customNight && this.state.currentNight === 7) { + this.startCustomNight(); + } else { + this.startGame(); + } + } + + showMainMenu() { + this.gameOverElement.classList.add('hidden'); + this.gameScreen.classList.remove('active'); + + // 显示音量按钮 + if (this.volumeBtn) { + this.volumeBtn.classList.remove('hidden'); + } + + // 关闭摄像头面板 + if (this.state.cameraOpen) { + this.camera.close(); + } + + // 隐藏摄像头面板 + const cameraPanel = document.getElementById('camera-panel'); + if (cameraPanel) { + cameraPanel.classList.add('hidden'); + cameraPanel.classList.remove('show'); + } + + // 清理角色图层 + const characterOverlay = document.getElementById('character-overlay'); + if (characterOverlay) { + characterOverlay.innerHTML = ''; + } + + // 隐藏控制面板 + const controlPanel = document.getElementById('control-panel'); + if (controlPanel) { + controlPanel.classList.add('hidden'); + } + + this.mainMenu.classList.remove('hidden'); + this.stopGame(); + + // 更新Continue按钮显示 + this.updateContinueButton(); + + this.assets.stopSound('vents'); + this.assets.stopSound('static'); + this.assets.stopSound('staticLoop'); + this.assets.stopSound('ventCrawling'); + + const menuMusic = document.getElementById('menu-music'); + if (menuMusic) { + menuMusic.loop = true; + menuMusic.currentTime = 0; + menuMusic.play().catch(e => console.log('Menu music playback failed:', e)); + } + } +} diff --git a/games/five-nights-at-epsteins/js/GameState.js b/games/five-nights-at-epsteins/js/GameState.js new file mode 100644 index 00000000..84ce2ffa --- /dev/null +++ b/games/five-nights-at-epsteins/js/GameState.js @@ -0,0 +1,43 @@ +// 游戏状态管理 +class GameState { + constructor() { + this.currentNight = 1; + this.maxNights = 5; // 当前版本有5晚(Night 1-5),Night 6 是特殊关卡 + this.currentTime = 0; // 0-6 (12AM-6AM) + this.oxygen = 100; // 氧气替代电量 + this.isGameRunning = false; + this.tutorialActive = false; // 教程是否激活 + this.currentScene = 'office'; + this.cameraOpen = false; + this.ventsClosed = false; // 通风口状态 + this.ventsToggling = false; // 通风口是否正在切换 + this.currentCam = 'cam11'; // 当前摄像头 + this.cameraFailed = false; // 摄像头是否故障 + this.cameraRestarting = false; // 摄像头是否正在重启 + this.controlPanelBusy = false; // 控制面板是否正在处理操作 + + // Custom Night 状态 + this.customNight = false; // 是否为自定义夜晚 + this.customAILevels = { + epstein: 0, + trump: 0, + hawking: 0 + }; + } + + reset() { + this.currentTime = 0; + this.oxygen = 100; + this.isGameRunning = true; + this.tutorialActive = false; + this.currentScene = 'office'; + this.cameraOpen = false; + this.ventsClosed = false; + this.ventsToggling = false; + this.currentCam = 'cam11'; + this.cameraFailed = false; + this.cameraRestarting = false; + this.controlPanelBusy = false; + // 注意:不重置 customNight 和 customAILevels,因为它们在 initGame 之前设置 + } +} diff --git a/games/five-nights-at-epsteins/js/InputHandler.js b/games/five-nights-at-epsteins/js/InputHandler.js new file mode 100644 index 00000000..e09dc32e --- /dev/null +++ b/games/five-nights-at-epsteins/js/InputHandler.js @@ -0,0 +1,264 @@ +// Input handler +class InputHandler { + constructor(game) { + this.game = game; + this.touchStartX = 0; + this.touchStartY = 0; + this.isTouching = false; + this.bindEvents(); + } + + bindEvents() { + // Keyboard controls + document.addEventListener('keydown', (e) => this.handleKeyPress(e)); + + // Mouse movement view control - edge trigger + const gameScreen = document.getElementById('game-screen'); + gameScreen.addEventListener('mousemove', (e) => this.handleMouseMove(e)); + + // Touch controls for mobile + gameScreen.addEventListener('touchstart', (e) => this.handleTouchStart(e), { passive: false }); + gameScreen.addEventListener('touchmove', (e) => this.handleTouchMove(e), { passive: false }); + gameScreen.addEventListener('touchend', (e) => this.handleTouchEnd(e), { passive: false }); + } + + handleKeyPress(e) { + // ==================== 作弊键(生产环境请注释掉) ==================== + + /* // F6 作弊键:立即触发特朗普进入管道(测试音效用) + if (e.key === 'F6') { + e.preventDefault(); + if (this.game.state.isGameRunning && this.game.enemyAI.trump.hasSpawned) { + console.log('🎮 CHEAT: Forcing Trump to crawl into vents...'); + this.showCheatNotification('Trump entering vents NOW!'); + + // 强制特朗普从 cam1 开始爬行 + this.game.enemyAI.trump.currentLocation = 'cam1'; + + // 立即播放音效(不等待延迟)- 音量改为1.0(最大值) + console.log('Playing crawling sound immediately...'); + this.game.assets.playSound('ventCrawling', true, 1.0); + + // 10秒后停止音效 + setTimeout(() => { + console.log('Stopping crawling sound...'); + this.game.assets.stopSound('ventCrawling'); + }, 10000); + } else if (this.game.state.isGameRunning) { + this.showCheatNotification('Trump not spawned yet!'); + } + return; + } + + // F9 作弊键:跳过当前夜晚(调试用) + if (e.key === 'F9') { + e.preventDefault(); + if (this.game.state.isGameRunning) { + console.log('🎮 CHEAT: Skipping current night...'); + + // 显示作弊提示 + this.showCheatNotification('Skipping Night ' + this.game.state.currentNight); + + // 延迟执行,让玩家看到提示 + setTimeout(() => { + this.game.winNight(); + }, 500); + } + return; + } + + // F10 作弊键:解锁特殊夜晚(调试用) + if (e.key === 'F10') { + e.preventDefault(); + console.log('🎮 CHEAT: Unlocking Special Night...'); + localStorage.setItem('night6Unlocked', 'true'); + this.showCheatNotification('Special Night Unlocked!'); + + // 如果在主菜单,立即更新按钮显示 + if (this.game.mainMenu && !this.game.mainMenu.classList.contains('hidden')) { + this.game.updateContinueButton(); + } + return; + } + + // F8 作弊键:解锁Custom Night(调试用) + if (e.key === 'F8') { + e.preventDefault(); + console.log('🎮 CHEAT: Unlocking Custom Night...'); + localStorage.setItem('night6Completed', 'true'); + this.showCheatNotification('Custom Night Unlocked!'); + + // 如果在主菜单,立即更新按钮显示 + if (this.game.mainMenu && !this.game.mainMenu.classList.contains('hidden')) { + this.game.updateContinueButton(); + } + return; + } + + // F7 作弊键:时间加速(测试用) + if (e.key === 'F7') { + e.preventDefault(); + if (this.game.state.isGameRunning) { + this.game.state.currentTime += 1; + this.game.ui.update(); + this.showCheatNotification(`Time: ${this.game.state.currentTime} AM`); + + if (this.game.state.currentTime >= 6) { + this.game.winNight(); + } + } + return; + } + + // 数字键1-6:快速跳到对应关卡(测试用,仅在主菜单有效) + if (e.key >= '1' && e.key <= '6') { + if (this.game.mainMenu && !this.game.mainMenu.classList.contains('hidden')) { + e.preventDefault(); + const night = parseInt(e.key); + console.log(`🎮 CHEAT: Jumping to Night ${night}...`); + this.game.state.currentNight = night; + this.showCheatNotification(`Starting Night ${night}`); + + // 如果是Night 6,需要先解锁 + if (night === 6) { + localStorage.setItem('night6Unlocked', 'true'); + setTimeout(() => this.game.startSpecialNight(), 500); + } else { + setTimeout(() => this.game.initGame(), 500); + } + + this.game.mainMenu.classList.add('hidden'); + const menuMusic = document.getElementById('menu-music'); + if (menuMusic) { + menuMusic.pause(); + menuMusic.currentTime = 0; + } + } + return; + } */ + + // ==================== 作弊键结束 ==================== + + if (!this.game.state.isGameRunning) return; + + switch(e.key.toLowerCase()) { + case 'v': + this.game.toggleVents(); + break; + case ' ': + e.preventDefault(); + this.game.toggleCamera(); + break; + } + } + + // 作弊通知 + showCheatNotification(message) { + // 创建通知元素 + const notification = document.createElement('div'); + notification.style.position = 'fixed'; + notification.style.top = '10px'; + notification.style.left = '50%'; + notification.style.transform = 'translateX(-50%)'; + notification.style.background = 'rgba(255, 215, 0, 0.9)'; + notification.style.color = '#000'; + notification.style.padding = '10px 20px'; + notification.style.fontSize = '20px'; + notification.style.fontWeight = 'bold'; + notification.style.fontFamily = 'Arial, sans-serif'; + notification.style.borderRadius = '5px'; + notification.style.zIndex = '99999'; + notification.style.boxShadow = '0 0 20px rgba(255, 215, 0, 0.8)'; + notification.textContent = '🎮 CHEAT: ' + message; + + document.body.appendChild(notification); + + // 1秒后移除 + setTimeout(() => { + notification.remove(); + }, 1000); + } + + handleMouseMove(e) { + if (!this.game.state.isGameRunning || this.game.state.cameraOpen) return; + + const edgeThreshold = 100; + const mouseX = e.clientX; + const screenWidth = window.innerWidth; + + // Check if at left edge + if (mouseX < edgeThreshold) { + this.game.isRotatingLeft = true; + this.game.isRotatingRight = false; + } + // Check if at right edge + else if (mouseX > screenWidth - edgeThreshold) { + this.game.isRotatingRight = true; + this.game.isRotatingLeft = false; + } + // In middle area, stop rotation + else { + this.game.isRotatingLeft = false; + this.game.isRotatingRight = false; + } + } + + handleTouchStart(e) { + if (!this.game.state.isGameRunning || this.game.state.cameraOpen) return; + + // Don't prevent default if touching UI elements + const target = e.target; + if (target.closest('.hotspot') || target.closest('.control-panel-button') || + target.closest('.camera-button') || target.closest('#control-panel-popup')) { + return; + } + + e.preventDefault(); + + const touch = e.touches[0]; + this.touchStartX = touch.clientX; + this.touchStartY = touch.clientY; + this.isTouching = true; + } + + handleTouchMove(e) { + if (!this.game.state.isGameRunning || this.game.state.cameraOpen || !this.isTouching) return; + + // Don't prevent default if touching UI elements + const target = e.target; + if (target.closest('.hotspot') || target.closest('.control-panel-button') || + target.closest('.camera-button') || target.closest('#control-panel-popup')) { + return; + } + + e.preventDefault(); + + const touch = e.touches[0]; + const deltaX = touch.clientX - this.touchStartX; + const deltaY = Math.abs(touch.clientY - this.touchStartY); + + // Only rotate if horizontal swipe (not vertical) + if (deltaY < 50) { + const sensitivity = 0.002; + // Reverse the direction: swipe right = view right, swipe left = view left + const movement = -deltaX * sensitivity; + + // Update view position directly + this.game.viewPosition += movement; + this.game.viewPosition = Math.max(0, Math.min(1, this.game.viewPosition)); + this.game.ui.updateViewPosition(this.game.viewPosition); + + // Update touch start position for smooth continuous movement + this.touchStartX = touch.clientX; + this.touchStartY = touch.clientY; + } + } + + handleTouchEnd(e) { + if (!this.game.state.isGameRunning) return; + + this.isTouching = false; + this.game.isRotatingLeft = false; + this.game.isRotatingRight = false; + } +} diff --git a/games/five-nights-at-epsteins/js/ScaryFaceFlicker.js b/games/five-nights-at-epsteins/js/ScaryFaceFlicker.js new file mode 100644 index 00000000..3db28a74 --- /dev/null +++ b/games/five-nights-at-epsteins/js/ScaryFaceFlicker.js @@ -0,0 +1,59 @@ +// 恐怖脸闪烁效果 +const basePath = './'; +const normalBackground = `${basePath}assets/images/menubackground.png`; +const scaryBackgrounds = [ + `${basePath}assets/images/scaryhawk.png`, + `${basePath}assets/images/scaryep.png`, + `${basePath}assets/images/scarytrump.png` +]; + +let scaryFaceInterval = null; +const preloadedImages = {}; + +// 预加载所有背景图片 +function preloadBackgrounds() { + const normalImg = new Image(); + normalImg.src = normalBackground; + preloadedImages['normal'] = normalImg; + + scaryBackgrounds.forEach((bg, index) => { + const img = new Image(); + img.src = bg; + preloadedImages[`scary-${index}`] = img; + }); +} + +function startScaryFaceFlicker() { + if (scaryFaceInterval) { + stopScaryFaceFlicker(); + } + + const mainMenu = document.getElementById('main-menu'); + if (!mainMenu) return; + + scaryFaceInterval = setInterval(() => { + if (Math.random() < 0.1) { + const bgIndex = Math.floor(Math.random() * 3); + const scaryBg = scaryBackgrounds[bgIndex]; + + mainMenu.style.backgroundImage = `url('${scaryBg}')`; + + const hideDelay = 50 + Math.random() * 150; + setTimeout(() => { + mainMenu.style.backgroundImage = `url('${normalBackground}')`; + }, hideDelay); + } + }, 100); +} + +function stopScaryFaceFlicker() { + if (scaryFaceInterval) { + clearInterval(scaryFaceInterval); + scaryFaceInterval = null; + + const mainMenu = document.getElementById('main-menu'); + if (mainMenu) { + mainMenu.style.backgroundImage = `url('${normalBackground}')`; + } + } +} diff --git a/games/five-nights-at-epsteins/js/StaticNoise.js b/games/five-nights-at-epsteins/js/StaticNoise.js new file mode 100644 index 00000000..3fc91726 --- /dev/null +++ b/games/five-nights-at-epsteins/js/StaticNoise.js @@ -0,0 +1,58 @@ +// 静态噪点效果 +class StaticNoise { + constructor() { + this.canvas = document.getElementById('static-canvas'); + if (!this.canvas) { + console.error('Static canvas not found'); + return; + } + + this.ctx = this.canvas.getContext('2d'); + this.isRunning = false; + this.animationId = null; + this.resize(); + window.addEventListener('resize', () => this.resize()); + } + + resize() { + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + } + + start() { + if (this.isRunning) return; + this.isRunning = true; + this.canvas.style.display = 'block'; + this.animate(); + } + + stop() { + this.isRunning = false; + this.canvas.style.display = 'none'; + if (this.animationId) { + cancelAnimationFrame(this.animationId); + this.animationId = null; + } + } + + animate() { + if (!this.ctx || !this.isRunning) return; + + const width = this.canvas.width; + const height = this.canvas.height; + const imageData = this.ctx.createImageData(width, height); + const data = imageData.data; + + // 生成更强烈的随机噪点(电视雪花效果) + for (let i = 0; i < data.length; i += 4) { + const value = Math.random() > 0.5 ? 255 : 0; + data[i] = value; + data[i + 1] = value; + data[i + 2] = value; + data[i + 3] = Math.random() * 255; + } + + this.ctx.putImageData(imageData, 0, 0); + this.animationId = requestAnimationFrame(() => this.animate()); + } +} diff --git a/games/five-nights-at-epsteins/js/UIManager.js b/games/five-nights-at-epsteins/js/UIManager.js new file mode 100644 index 00000000..812b0f1a --- /dev/null +++ b/games/five-nights-at-epsteins/js/UIManager.js @@ -0,0 +1,609 @@ +// UI Manager +class UIManager { + constructor(game) { + this.game = game; + this.initElements(); + } + + initElements() { + this.powerValue = document.getElementById('power-value'); + this.timeValue = document.getElementById('time-value'); + this.nightValue = document.getElementById('night-value'); + this.currentSceneImg = document.getElementById('current-scene'); + } + + update() { + this.powerValue.textContent = Math.floor(this.game.state.oxygen); + this.timeValue.textContent = `${this.game.state.currentTime === 0 ? 12 : this.game.state.currentTime} AM`; + + // Custom Night 显示为 "7" 或 "CUSTOM" + if (this.game.state.customNight && this.game.state.currentNight === 7) { + this.nightValue.textContent = '7'; + } else { + this.nightValue.textContent = this.game.state.currentNight; + } + + // Only update scene image when camera is not open + if (!this.game.state.cameraOpen) { + const sceneKey = 'office'; + if (this.game.assets.images[sceneKey]) { + this.currentSceneImg.src = this.game.assets.images[sceneKey].src; + this.currentSceneImg.style.display = 'block'; + } + } + + // Flash warning when oxygen below 40% + if (this.game.state.oxygen <= 40 && this.game.state.ventsClosed) { + this.powerValue.classList.add('flicker'); + } else { + this.powerValue.classList.remove('flicker'); + } + + // Update camera status + this.updateCameraStatus(); + } + + createHotspots() { + const hotspotsContainer = document.getElementById('hotspots'); + hotspotsContainer.innerHTML = ''; + + // Create control panel button (special style) + this.createControlPanelButton(); + + // Create camera button (special style) + this.createCameraButton(); + + // Bind close camera button event + this.bindCloseCameraButton(); + } + + createControlPanelButton() { + const hotspotsContainer = document.getElementById('hotspots'); + + const controlBtn = document.createElement('div'); + controlBtn.id = 'vents-btn'; + controlBtn.className = 'control-panel-button'; + controlBtn.style.position = 'absolute'; + controlBtn.style.left = '0'; + controlBtn.style.bottom = '0'; + controlBtn.style.width = '25vw'; + controlBtn.style.height = '10vh'; + controlBtn.style.background = 'rgba(0, 0, 0, 0.7)'; + controlBtn.style.border = '2px solid rgba(255, 255, 255, 0.3)'; + controlBtn.style.borderLeft = 'none'; + controlBtn.style.borderBottom = 'none'; + controlBtn.style.display = 'flex'; + controlBtn.style.flexDirection = 'row'; + controlBtn.style.alignItems = 'center'; + controlBtn.style.justifyContent = 'space-between'; + controlBtn.style.cursor = 'pointer'; + controlBtn.style.opacity = '0'; + controlBtn.style.transition = 'opacity 0.3s, background 0.3s'; + controlBtn.style.padding = '0 1.5vw'; + controlBtn.style.pointerEvents = 'none'; + + // Left arrows container + const leftArrows = document.createElement('div'); + leftArrows.style.display = 'flex'; + leftArrows.style.gap = '0.8vw'; + leftArrows.className = 'control-arrows'; + leftArrows.style.flexShrink = '0'; + + const arrowLeft1 = document.createElement('div'); + arrowLeft1.innerHTML = '▲'; + arrowLeft1.style.color = '#fff'; + arrowLeft1.style.fontSize = '2vw'; + arrowLeft1.style.lineHeight = '1'; + leftArrows.appendChild(arrowLeft1); + + const arrowLeft2 = document.createElement('div'); + arrowLeft2.innerHTML = '▲'; + arrowLeft2.style.color = '#fff'; + arrowLeft2.style.fontSize = '2vw'; + arrowLeft2.style.lineHeight = '1'; + leftArrows.appendChild(arrowLeft2); + + controlBtn.appendChild(leftArrows); + + // CONTROL PANEL text + const text = document.createElement('div'); + text.textContent = 'CONTROL PANEL'; + text.style.color = '#fff'; + text.style.fontSize = '1.8vw'; + text.style.fontWeight = 'bold'; + text.style.fontFamily = 'Arial, sans-serif'; + text.style.letterSpacing = '0.15vw'; + text.style.whiteSpace = 'nowrap'; + text.style.flex = '1'; + text.style.textAlign = 'center'; + controlBtn.appendChild(text); + + // Right arrows container + const rightArrows = document.createElement('div'); + rightArrows.style.display = 'flex'; + rightArrows.style.gap = '0.8vw'; + rightArrows.className = 'control-arrows'; + rightArrows.style.flexShrink = '0'; + + const arrowRight1 = document.createElement('div'); + arrowRight1.innerHTML = '▲'; + arrowRight1.style.color = '#fff'; + arrowRight1.style.fontSize = '2vw'; + arrowRight1.style.lineHeight = '1'; + rightArrows.appendChild(arrowRight1); + + const arrowRight2 = document.createElement('div'); + arrowRight2.innerHTML = '▲'; + arrowRight2.style.color = '#fff'; + arrowRight2.style.fontSize = '2vw'; + arrowRight2.style.lineHeight = '1'; + rightArrows.appendChild(arrowRight2); + + controlBtn.appendChild(rightArrows); + + // Hover effect + controlBtn.addEventListener('mouseenter', () => { + controlBtn.style.background = 'rgba(0, 0, 0, 0.9)'; + }); + + controlBtn.addEventListener('mouseleave', () => { + controlBtn.style.background = 'rgba(0, 0, 0, 0.7)'; + }); + + // Click event - open control panel popup + controlBtn.addEventListener('click', () => { + this.toggleControlPanel(); + setTimeout(() => this.updateControlPanelArrows(), 50); + }); + + hotspotsContainer.appendChild(controlBtn); + } + + updateControlPanelArrows() { + const controlBtn = document.getElementById('vents-btn'); + if (!controlBtn) return; + + const arrows = controlBtn.querySelectorAll('.control-arrows div'); + const isOpen = document.getElementById('control-panel-popup') && + !document.getElementById('control-panel-popup').classList.contains('hidden'); + + // Before panel opens: arrows point up ▲ + // Before panel closes: arrows point down ▼ + arrows.forEach((arrow) => { + arrow.innerHTML = isOpen ? '▼' : '▲'; + }); + } + + toggleControlPanel() { + const panel = document.getElementById('control-panel-popup'); + if (panel) { + const wasHidden = panel.classList.contains('hidden'); + + // 如果要关闭面板,检查是否有操作正在进行 + if (!wasHidden && this.game.state.controlPanelBusy) { + // console.log('Cannot close control panel: operation in progress'); + return; // 阻止关闭,不显示任何消息 + } + + panel.classList.toggle('hidden'); + + // Control view rotation + if (wasHidden) { + // Open panel, stop rotation + this.game.isRotatingLeft = false; + this.game.isRotatingRight = false; + this.game.state.controlPanelOpen = true; + } else { + // Close panel + this.game.state.controlPanelOpen = false; + } + } else { + this.createControlPanelPopup(); + this.game.isRotatingLeft = false; + this.game.isRotatingRight = false; + this.game.state.controlPanelOpen = true; + } + } + + createControlPanelPopup() { + const popup = document.createElement('div'); + popup.id = 'control-panel-popup'; + popup.style.position = 'fixed'; + popup.style.top = '10vh'; + popup.style.left = '10vw'; + popup.style.width = '70vw'; + popup.style.minHeight = '60vh'; + popup.style.background = '#000'; + popup.style.border = '4px solid #0f0'; + popup.style.padding = '4vh 4vw'; + popup.style.zIndex = '100'; + popup.style.fontFamily = "'Courier New', monospace"; + popup.style.color = '#0f0'; + + // Title + const title = document.createElement('div'); + title.textContent = '/// Control Panel'; + title.style.fontSize = '2.5vw'; + title.style.fontWeight = 'bold'; + title.style.marginBottom = '5vh'; + popup.appendChild(title); + + // Options container + const optionsContainer = document.createElement('div'); + optionsContainer.id = 'control-options'; + + // Option 1: Air Vents + const option1 = document.createElement('div'); + option1.id = 'option-vents'; + option1.style.fontSize = '2.5vw'; + option1.style.marginBottom = '4vh'; + option1.style.cursor = 'pointer'; + option1.style.padding = '1.5vh 0'; + option1.style.display = 'flex'; + option1.style.alignItems = 'center'; + option1.style.direction = 'ltr'; // 强制从左到右 + option1.innerHTML = this.game.state.ventsClosed ? + '>Open Air Vents' : + '>Close Air Vents'; + option1.addEventListener('click', () => { + this.game.toggleVents(); + // 不在这里立即更新,等toggleVents完成后会自动调用updateControlPanelOptions + }); + optionsContainer.appendChild(option1); + + // Option 2: Restart Cameras + const option2 = document.createElement('div'); + option2.id = 'option-cameras'; + option2.style.fontSize = '2.5vw'; + option2.style.cursor = 'pointer'; + option2.style.padding = '1.5vh 0'; + option2.style.display = 'flex'; + option2.style.alignItems = 'center'; + option2.style.direction = 'ltr'; // 强制从左到右 + option2.innerHTML = '>Restart Cameras'; + option2.addEventListener('click', () => { + this.selectControlOption('cameras'); + this.handleRestartCamera(); + }); + optionsContainer.appendChild(option2); + + popup.appendChild(optionsContainer); + + // Click outside to close (only if no operation in progress) + document.addEventListener('click', (e) => { + if (!popup.contains(e.target) && e.target.id !== 'vents-btn' && !e.target.closest('#vents-btn')) { + // 检查是否有操作正在进行 + if (this.game.state.controlPanelBusy) { + // console.log('Cannot close control panel: operation in progress'); + return; // 阻止关闭,不显示任何消息 + } + + popup.classList.add('hidden'); + this.game.state.controlPanelOpen = false; + this.updateControlPanelArrows(); + } + }); + + document.body.appendChild(popup); + } + + selectControlOption(option) { + const option1 = document.getElementById('option-vents'); + const option2 = document.getElementById('option-cameras'); + + if (option === 'vents') { + const arrow1 = option1.querySelector('.option-arrow'); + const arrow2 = option2.querySelector('.option-arrow'); + if (arrow1) arrow1.style.color = '#0f0'; + if (arrow2) arrow2.style.color = 'transparent'; + + // 更新通风口文本(不包括dots span) + const text1 = option1.querySelector('span:nth-child(2)'); + if (text1) { + text1.textContent = this.game.state.ventsClosed ? 'Open Air Vents' : 'Close Air Vents'; + } + } else { + const arrow1 = option1.querySelector('.option-arrow'); + const arrow2 = option2.querySelector('.option-arrow'); + if (arrow1) arrow1.style.color = 'transparent'; + if (arrow2) arrow2.style.color = '#0f0'; + } + } + + updateControlPanelOptions() { + this.selectControlOption('vents'); + this.updateCameraStatus(); + this.updateVentsStatus(); // 添加通风口状态更新 + } + + // Update vents status display (dots animation) + updateVentsStatus() { + const dotsSpan = document.getElementById('vents-dots'); + if (!dotsSpan) return; + + if (this.game.state.ventsToggling) { + // 正在切换,显示点动画 + dotsSpan.style.color = '#0f0'; // Green dots + if (!dotsSpan.dataset.animating) { + dotsSpan.dataset.animating = 'true'; + this.animateLoadingDots(dotsSpan); + } + } else { + // 不在切换中,清空点 + dotsSpan.textContent = ''; + delete dotsSpan.dataset.animating; + } + } + + // Update camera status display + updateCameraStatus() { + const statusSpan = document.getElementById('camera-status'); + const dotsSpan = document.getElementById('camera-dots'); + if (!statusSpan) return; + + if (this.game.state.cameraRestarting) { + // Restarting, show dots after button + if (dotsSpan) { + dotsSpan.style.color = '#0f0'; // Green dots + if (!dotsSpan.dataset.animating) { + dotsSpan.dataset.animating = 'true'; + this.animateLoadingDots(dotsSpan); + } + } + // 只有在摄像头确实故障时才显示ERR + if (this.game.state.cameraFailed) { + statusSpan.style.color = '#f00'; + statusSpan.textContent = 'ERR'; + } else { + // 没有故障时,重启期间不显示ERR + statusSpan.textContent = ''; + } + } else if (this.game.state.cameraFailed) { + // Failed, show ERR on right, no dots + if (dotsSpan) { + dotsSpan.textContent = ''; + delete dotsSpan.dataset.animating; + } + statusSpan.style.color = '#f00'; + statusSpan.textContent = 'ERR'; + } else { + // Normal, don't show anything + if (dotsSpan) { + dotsSpan.textContent = ''; + delete dotsSpan.dataset.animating; + } + statusSpan.textContent = ''; + } + } + + // Animate loading dots (green dots after button) + animateLoadingDots(element) { + const states = ['.', '..', '...']; + let index = 0; + + const animate = () => { + if (!element.dataset.animating) return; + + element.textContent = states[index]; + // console.log('Dots animation:', states[index]); // 调试输出 + index = (index + 1) % states.length; + + setTimeout(animate, 500); // Switch every 0.5s + }; + + animate(); + } + + // Animate display (dots after button, ERR on right) - 不再使用 + animateLoadingDotsWithERR(element) { + // 已废弃,使用 animateLoadingDots 代替 + } + + // Handle restart camera + handleRestartCamera() { + // 允许在摄像头没有故障时也能重启(作为策略使用) + if (!this.game.state.cameraRestarting && !this.game.state.controlPanelBusy) { + // console.log('Restarting cameras...'); + this.game.camera.restartCamera(); + + // Immediately update status display (show loading animation, but ERR doesn't disappear) + this.updateCameraStatus(); + + // Update status display every 100ms + const updateInterval = setInterval(() => { + this.updateCameraStatus(); + if (!this.game.state.cameraRestarting) { + clearInterval(updateInterval); + } + }, 100); + } + } + + createCameraButton() { + const hotspotsContainer = document.getElementById('hotspots'); + + const cameraBtn = document.createElement('div'); + cameraBtn.id = 'camera-btn'; + cameraBtn.className = 'camera-button'; + cameraBtn.style.position = 'absolute'; + cameraBtn.style.right = '0'; + cameraBtn.style.top = '25%'; + cameraBtn.style.width = '6vw'; + cameraBtn.style.height = '45vh'; + cameraBtn.style.background = 'rgba(0, 0, 0, 0.7)'; + cameraBtn.style.border = '2px solid rgba(255, 255, 255, 0.3)'; + cameraBtn.style.borderRight = 'none'; + cameraBtn.style.display = 'flex'; + cameraBtn.style.flexDirection = 'column'; + cameraBtn.style.alignItems = 'center'; + cameraBtn.style.justifyContent = 'space-between'; + cameraBtn.style.cursor = 'pointer'; + cameraBtn.style.opacity = '0'; + cameraBtn.style.transition = 'opacity 0.3s, background 0.3s'; + cameraBtn.style.padding = '2vh 0'; + cameraBtn.style.pointerEvents = 'none'; + + // Top arrows container + const topArrows = document.createElement('div'); + topArrows.style.display = 'flex'; + topArrows.style.flexDirection = 'column'; + topArrows.style.gap = '0.5vh'; + + // Top arrow (points left when closed) + const arrowTop = document.createElement('div'); + arrowTop.innerHTML = '◄'; + arrowTop.className = 'camera-arrow'; + arrowTop.style.color = '#fff'; + arrowTop.style.fontSize = '1.8vw'; + arrowTop.style.transform = 'rotate(0deg)'; + arrowTop.style.lineHeight = '1'; + topArrows.appendChild(arrowTop); + + // Second arrow + const arrowTop2 = document.createElement('div'); + arrowTop2.innerHTML = '◄'; + arrowTop2.className = 'camera-arrow'; + arrowTop2.style.color = '#fff'; + arrowTop2.style.fontSize = '1.8vw'; + arrowTop2.style.transform = 'rotate(0deg)'; + arrowTop2.style.lineHeight = '1'; + topArrows.appendChild(arrowTop2); + + cameraBtn.appendChild(topArrows); + + // CAMERA text (horizontal text rotated 90 degrees counterclockwise) + const text = document.createElement('div'); + text.textContent = 'CAMERA'; + text.style.color = '#fff'; + text.style.fontSize = '1.3vw'; + text.style.fontWeight = 'bold'; + text.style.fontFamily = 'Arial, sans-serif'; + text.style.transform = 'rotate(-90deg)'; + text.style.letterSpacing = '0.2vw'; + text.style.whiteSpace = 'nowrap'; + cameraBtn.appendChild(text); + + // Bottom arrows container + const bottomArrows = document.createElement('div'); + bottomArrows.style.display = 'flex'; + bottomArrows.style.flexDirection = 'column'; + bottomArrows.style.gap = '0.5vh'; + + // Bottom arrow + const arrowBottom = document.createElement('div'); + arrowBottom.innerHTML = '◄'; + arrowBottom.className = 'camera-arrow'; + arrowBottom.style.color = '#fff'; + arrowBottom.style.fontSize = '1.8vw'; + arrowBottom.style.transform = 'rotate(0deg)'; + arrowBottom.style.lineHeight = '1'; + bottomArrows.appendChild(arrowBottom); + + // Second bottom arrow + const arrowBottom2 = document.createElement('div'); + arrowBottom2.innerHTML = '◄'; + arrowBottom2.className = 'camera-arrow'; + arrowBottom2.style.color = '#fff'; + arrowBottom2.style.fontSize = '1.8vw'; + arrowBottom2.style.transform = 'rotate(0deg)'; + arrowBottom2.style.lineHeight = '1'; + bottomArrows.appendChild(arrowBottom2); + + cameraBtn.appendChild(bottomArrows); + + // Hover effect + cameraBtn.addEventListener('mouseenter', () => { + cameraBtn.style.background = 'rgba(0, 0, 0, 0.9)'; + }); + + cameraBtn.addEventListener('mouseleave', () => { + cameraBtn.style.background = 'rgba(0, 0, 0, 0.7)'; + }); + + // Click event + cameraBtn.addEventListener('click', () => { + // console.log('📷 Camera button clicked!'); + this.game.toggleCamera(); + // Delay arrow update, wait for state change + setTimeout(() => this.updateCameraButtonArrows(), 50); + }); + + hotspotsContainer.appendChild(cameraBtn); + } + + bindCloseCameraButton() { + // Close button removed - camera button is now always accessible + } + + updateCameraButtonArrows() { + const cameraBtn = document.getElementById('camera-btn'); + if (!cameraBtn) return; + + const arrows = cameraBtn.querySelectorAll('.camera-arrow'); + const isOpen = this.game.state.cameraOpen; + + // Update arrow direction + // Before opening (closed state): arrows point left (0deg) + // Before closing (open state): arrows point right (180deg) + arrows.forEach((arrow) => { + arrow.style.transform = isOpen ? 'rotate(180deg)' : 'rotate(0deg)'; + }); + } + + updateHotspotVisibility(viewPosition) { + const ventsBtn = document.getElementById('vents-btn'); + const cameraBtn = document.getElementById('camera-btn'); + + // console.log('🔍 updateHotspotVisibility - viewPosition:', viewPosition); + + // Show control panel when view is at far left (viewPosition = 0) + if (ventsBtn) { + ventsBtn.style.opacity = viewPosition < 0.15 ? '1' : '0'; + ventsBtn.style.pointerEvents = viewPosition < 0.15 ? 'auto' : 'none'; + } + + // Show camera button when view is at far right (viewPosition = 1) + if (cameraBtn) { + const isVisible = viewPosition > 0.85; + cameraBtn.style.opacity = isVisible ? '1' : '0'; + cameraBtn.style.pointerEvents = isVisible ? 'auto' : 'none'; + // console.log('📷 Camera button - opacity:', cameraBtn.style.opacity, 'pointerEvents:', cameraBtn.style.pointerEvents); + } + } + + showTooltip(event, text) { + let tooltip = document.getElementById('game-tooltip'); + if (!tooltip) { + tooltip = document.createElement('div'); + tooltip.id = 'game-tooltip'; + tooltip.style.position = 'fixed'; + tooltip.style.background = 'rgba(0, 0, 0, 0.8)'; + tooltip.style.color = 'white'; + tooltip.style.padding = '8px 12px'; + tooltip.style.borderRadius = '4px'; + tooltip.style.fontSize = '14px'; + tooltip.style.pointerEvents = 'none'; + tooltip.style.zIndex = '10000'; + tooltip.style.whiteSpace = 'nowrap'; + document.body.appendChild(tooltip); + } + + tooltip.textContent = text; + tooltip.style.display = 'block'; + tooltip.style.left = event.clientX + 10 + 'px'; + tooltip.style.top = event.clientY + 10 + 'px'; + } + + hideTooltip() { + const tooltip = document.getElementById('game-tooltip'); + if (tooltip) { + tooltip.style.display = 'none'; + } + } + + updateViewPosition(viewPosition) { + const offset = -viewPosition * 50; + this.currentSceneImg.style.left = `${offset}%`; + this.updateHotspotVisibility(viewPosition); + } +} diff --git a/games/five-nights-at-epsteins/js/main.js b/games/five-nights-at-epsteins/js/main.js new file mode 100644 index 00000000..5c813cc8 --- /dev/null +++ b/games/five-nights-at-epsteins/js/main.js @@ -0,0 +1,325 @@ +// 游戏入口 - 初始化所有模块 +let game; +let staticNoise; + +// 预加载进度跟踪 +let loadedAssets = 0; +let totalAssets = 0; + +// 禁用浏览器默认行为,提升游戏体验 +function disableBrowserDefaults() { + // 禁用右键菜单 + document.addEventListener('contextmenu', (e) => { + e.preventDefault(); + return false; + }, { capture: true }); + + // 禁用拖拽 + document.addEventListener('dragstart', (e) => { + e.preventDefault(); + return false; + }, { capture: true }); + + // 禁用选择文本(双击、长按等) + document.addEventListener('selectstart', (e) => { + e.preventDefault(); + return false; + }, { capture: true }); + + // 禁用复制 + document.addEventListener('copy', (e) => { + e.preventDefault(); + return false; + }, { capture: true }); + + // 禁用剪切 + document.addEventListener('cut', (e) => { + e.preventDefault(); + return false; + }, { capture: true }); + + // 禁用某些快捷键 + document.addEventListener('keydown', (e) => { + // 禁用 Ctrl+A (全选) + if (e.ctrlKey && e.key === 'a') { + e.preventDefault(); + return false; + } + // 禁用 Ctrl+C (复制) + if (e.ctrlKey && e.key === 'c') { + e.preventDefault(); + return false; + } + // 禁用 Ctrl+X (剪切) + if (e.ctrlKey && e.key === 'x') { + e.preventDefault(); + return false; + } + // 禁用 Ctrl+S (保存) + if (e.ctrlKey && e.key === 's') { + e.preventDefault(); + return false; + } + // 禁用 Ctrl+P (打印) + if (e.ctrlKey && e.key === 'p') { + e.preventDefault(); + return false; + } + // 禁用 Ctrl+U (查看源代码) + if (e.ctrlKey && e.key === 'u') { + e.preventDefault(); + return false; + } + }, { capture: true }); + + // 禁用触摸设备的长按菜单 + document.addEventListener('touchstart', (e) => { + if (e.touches.length > 1) { + e.preventDefault(); + } + }, { passive: false, capture: true }); + + // 禁用双指缩放 + document.addEventListener('touchmove', (e) => { + if (e.touches.length > 1) { + e.preventDefault(); + } + }, { passive: false, capture: true }); + + // 阻止鼠标选择文本 + document.addEventListener('mousedown', (e) => { + // 允许按钮点击 + if (e.target.tagName === 'BUTTON' || e.target.closest('button')) { + return true; + } + // 阻止其他元素的鼠标按下(防止拖拽选择) + if (e.detail > 1) { // 双击或多击 + e.preventDefault(); + return false; + } + }, { capture: true }); + + // console.log('Browser defaults disabled for better game experience'); +} + +// 更新预加载进度 +function updatePreloadProgress(progress) { + const progressBar = document.getElementById('progress-bar'); + const percentage = document.getElementById('preloader-percentage'); + + if (progressBar && percentage) { + progressBar.style.width = progress + '%'; + percentage.textContent = Math.round(progress) + '%'; + } +} + +// 预加载所有游戏资源 +async function preloadGameAssets() { + const basePath = './'; + + // 定义所有需要预加载的资源 + const imagePaths = [ + 'assets/images/original.png', + 'assets/images/Cam1.png', + 'assets/images/Cam2.png', + 'assets/images/Cam3.png', + 'assets/images/Cam4.png', + 'assets/images/Cam5.png', + 'assets/images/Cam6.png', + 'assets/images/Cam7.png', + 'assets/images/Cam8.png', + 'assets/images/Cam9.png', + 'assets/images/Cam10.png', + 'assets/images/Cam11.png', + 'assets/images/jump.png', + 'assets/images/menubackground.png', + 'assets/images/cutscene.png', + 'assets/images/fa3.png', + 'assets/images/FNAE-Map-layout.png', + 'assets/images/enemyep1.png', + 'assets/images/ep1.png', + 'assets/images/ep4.png', + 'assets/images/enemyep4.png', + 'assets/images/scaryhawk.png', + 'assets/images/scaryep.png', + 'assets/images/scarytrump.png', + 'assets/images/winscreen.png', // Night 5 胜利画面 + 'assets/images/goldenstephen.png' // Golden 霍金 + ]; + + const soundPaths = [ + 'assets/sounds/music.ogg', + 'assets/sounds/music3.ogg', + 'assets/sounds/Static_sound.ogg', + 'assets/sounds/vents.ogg', + 'assets/sounds/jumpcare.ogg', + 'assets/sounds/Blip.ogg', + 'assets/sounds/winmusic.ogg', + 'assets/sounds/chimes.ogg', + 'assets/sounds/Crank1.ogg', + 'assets/sounds/Crank2.ogg', + 'assets/sounds/goldenstephenscare.ogg' // Golden 霍金音效 + ]; + + totalAssets = imagePaths.length + soundPaths.length; + loadedAssets = 0; + + // 预加载图片 + const imagePromises = imagePaths.map(path => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + loadedAssets++; + updatePreloadProgress((loadedAssets / totalAssets) * 100); + resolve(); + }; + img.onerror = () => { + console.warn(`Failed to load image: ${path}`); + loadedAssets++; + updatePreloadProgress((loadedAssets / totalAssets) * 100); + resolve(); + }; + img.src = basePath + path; + }); + }); + + // 预加载音频(不阻塞,快速加载) + const audioPromises = soundPaths.map(path => { + return new Promise((resolve) => { + const audio = new Audio(); + audio.addEventListener('canplaythrough', () => { + loadedAssets++; + updatePreloadProgress((loadedAssets / totalAssets) * 100); + resolve(); + }, { once: true }); + audio.addEventListener('error', () => { + console.warn(`Failed to load audio: ${path}`); + loadedAssets++; + updatePreloadProgress((loadedAssets / totalAssets) * 100); + resolve(); + }, { once: true }); + audio.src = basePath + path; + audio.load(); + }); + }); + + // 等待所有资源加载完成 + await Promise.all([...imagePromises, ...audioPromises]); + + // 确保进度条显示100% + updatePreloadProgress(100); + + // 等待一小段时间让玩家看到100% + await new Promise(resolve => setTimeout(resolve, 500)); +} + +// 隐藏预加载动画 +function hidePreloader() { + const preloader = document.getElementById('preloader'); + if (preloader) { + preloader.classList.add('fade-out'); + setTimeout(() => { + preloader.style.display = 'none'; + }, 500); + } +} + +// 页面加载完成后启动 +window.addEventListener('DOMContentLoaded', async () => { + // 禁用浏览器默认行为 + disableBrowserDefaults(); + + // 先预加载所有资源 + await preloadGameAssets(); + + // 预加载背景图片(用于恐怖脸效果) + preloadBackgrounds(); + + // 隐藏预加载动画 + hidePreloader(); + + // 初始化游戏 + game = new Game(); + staticNoise = new StaticNoise(); + + // 更新Continue按钮显示 + game.updateContinueButton(); + + const mainMenu = document.getElementById('main-menu'); + + // 检查是否从外部页面启动(带autostart参数) + const urlParams = new URLSearchParams(window.location.search); + const autostart = urlParams.get('autostart'); + + // 启动菜单音乐 + const menuMusic = document.getElementById('menu-music'); + if (menuMusic) { + menuMusic.volume = 0.5; + + // 如果是autostart,立即尝试播放 + if (autostart === '1') { + // console.log('检测到autostart参数,尝试自动播放音乐...'); + menuMusic.play().then(() => { + // console.log('✅ 音乐自动播放成功!'); + }).catch(e => { + // console.log('❌ 自动播放失败,等待用户交互:', e); + // 失败则等待用户点击 + setupManualPlayback(); + }); + } else { + // 正常流程:等待用户点击 + setupManualPlayback(); + } + + function setupManualPlayback() { + const playMusic = () => { + if (mainMenu && !mainMenu.classList.contains('hidden')) { + menuMusic.play().catch(e => {/* console.log('音乐播放需要用户交互') */}); + } + document.removeEventListener('click', playMusic); + document.removeEventListener('keydown', playMusic); + }; + + document.addEventListener('click', playMusic); + document.addEventListener('keydown', playMusic); + } + } + + // 监听主菜单显示/隐藏,控制雪花和鬼脸效果 + const observer = new MutationObserver(() => { + if (mainMenu && !mainMenu.classList.contains('hidden')) { + startScaryFaceFlicker(); + staticNoise.start(); + } else { + stopScaryFaceFlicker(); + staticNoise.stop(); + } + }); + + if (mainMenu) { + observer.observe(mainMenu, { attributes: true, attributeFilter: ['class'] }); + + if (!mainMenu.classList.contains('hidden')) { + startScaryFaceFlicker(); + staticNoise.start(); + } + } +}); + +// 监听来自父页面的消息(iframe 通信) +window.addEventListener('message', (event) => { + if (event.data.type === 'USER_CLICKED_PLAY') { + // console.log('收到父页面的用户点击事件'); + const menuMusic = document.getElementById('menu-music'); + if (menuMusic) { + // 立即尝试播放音乐 + menuMusic.volume = 0.5; + menuMusic.play().then(() => { + // console.log('✅ 音乐自动播放成功!'); + }).catch(e => { + // console.log('❌ 音乐播放失败:', e); + // 如果失败,等待用户在游戏内点击 + }); + } + } +}); diff --git a/games/five-nights-at-epsteins/preview.png b/games/five-nights-at-epsteins/preview.png new file mode 100644 index 00000000..5570284e Binary files /dev/null and b/games/five-nights-at-epsteins/preview.png differ diff --git a/games/five-nights-at-epsteins/style.css b/games/five-nights-at-epsteins/style.css new file mode 100644 index 00000000..9f25bb7b --- /dev/null +++ b/games/five-nights-at-epsteins/style.css @@ -0,0 +1,2140 @@ +html { + width: 100%; + height: 100%; + overflow: hidden; +} + +body { + font-family: 'Arial', sans-serif; + background: #000; + overflow: hidden; + color: #fff; + margin: 0; + padding: 0; + width: 100%; + height: 100%; + /* 禁用文本选择 */ + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + /* 禁用触摸时的高亮 */ + -webkit-tap-highlight-color: transparent; + /* 禁用拖拽 */ + -webkit-user-drag: none; + -khtml-user-drag: none; + -moz-user-drag: none; + -o-user-drag: none; + user-drag: none; +} + +/* 禁用所有元素的选择和拖拽 - 使用!important确保优先级 */ +* { + user-select: none !important; + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + -webkit-user-drag: none !important; + -khtml-user-drag: none !important; + -moz-user-drag: none !important; + -o-user-drag: none !important; + user-drag: none !important; + -webkit-touch-callout: none !important; +} + +/* 特别针对文本元素 */ +div, +span, +p, +h1, +h2, +h3, +h4, +h5, +h6, +button, +a, +label { + user-select: none !important; + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; +} + +/* 禁用图片拖拽 */ +img { + -webkit-user-drag: none !important; + -khtml-user-drag: none !important; + -moz-user-drag: none !important; + -o-user-drag: none !important; + user-drag: none !important; + pointer-events: none; +} + +/* 确保按钮可以点击但不能选择文本 */ +button { + pointer-events: auto; + cursor: pointer; +} + +/* 预加载动画 */ +#preloader { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #000; + display: flex; + align-items: center; + justify-content: center; + z-index: 99999; + transition: opacity 0.5s ease-out; +} + +#preloader.fade-out { + opacity: 0; + pointer-events: none; +} + +.preloader-content { + text-align: center; + max-width: 500px; + padding: 20px; +} + +.preloader-logo { + font-size: 3vw; + font-weight: bold; + color: #ff0000; + text-shadow: + 0 0 10px #ff0000, + 0 0 20px #ff0000, + 0 0 30px #ff0000, + 2px 2px 4px #000; + margin-bottom: 40px; + font-family: 'Courier New', monospace; + letter-spacing: 0.2em; + line-height: 1.3; + animation: glowPulse 2s ease-in-out infinite; +} + +@keyframes glowPulse { + + 0%, + 100% { + text-shadow: + 0 0 10px #ff0000, + 0 0 20px #ff0000, + 0 0 30px #ff0000, + 2px 2px 4px #000; + } + + 50% { + text-shadow: + 0 0 20px #ff0000, + 0 0 30px #ff0000, + 0 0 40px #ff0000, + 0 0 50px #ff0000, + 2px 2px 4px #000; + } +} + +.preloader-spinner { + position: relative; + width: 80px; + height: 80px; + margin: 0 auto 30px; +} + +.spinner-ring { + position: absolute; + width: 100%; + height: 100%; + border: 3px solid transparent; + border-top-color: #ff0000; + border-radius: 50%; + animation: spin 1.5s linear infinite; +} + +.spinner-ring:nth-child(2) { + width: 70%; + height: 70%; + top: 15%; + left: 15%; + border-top-color: #cc0000; + animation-duration: 1.2s; + animation-direction: reverse; +} + +.spinner-ring:nth-child(3) { + width: 40%; + height: 40%; + top: 30%; + left: 30%; + border-top-color: #990000; + animation-duration: 0.9s; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.preloader-text { + font-size: 1.5vw; + color: #fff; + font-family: 'Courier New', monospace; + margin-bottom: 20px; + letter-spacing: 0.3em; +} + +.loading-dots::after { + content: '...'; + animation: loadingDots 1.5s steps(4, end) infinite; +} + +@keyframes loadingDots { + + 0%, + 20% { + content: '.'; + } + + 40% { + content: '..'; + } + + 60%, + 100% { + content: '...'; + } +} + +.preloader-progress { + width: 100%; + height: 4px; + background: #333; + border-radius: 2px; + overflow: hidden; + margin-bottom: 10px; +} + +.progress-bar { + height: 100%; + background: linear-gradient(90deg, #ff0000, #cc0000); + width: 0%; + transition: width 0.3s ease; + box-shadow: 0 0 10px #ff0000; +} + +.preloader-percentage { + font-size: 1.2vw; + color: #888; + font-family: 'Courier New', monospace; +} + +/* 通用隐藏类 */ +.hidden { + display: none !important; +} + +/* 加载动画 */ +.loading { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 24px; + color: #fff; + font-family: 'Courier New', monospace; + z-index: 1000; + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.3; + } +} + +#game-container { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; +} + +/* 游戏画面 */ +#game-screen { + width: 100%; + height: 100%; + position: relative; + display: none; + overflow: hidden; + background: #000; +} + +#game-screen.active { + display: block; +} + +#current-scene { + position: absolute; + height: 100%; + width: 150%; + object-fit: cover; + left: 0; + top: 0; + transition: left 0.05s linear; + display: block; +} + +/* 热区 */ +#hotspots { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.hotspot { + position: absolute; + cursor: pointer; + pointer-events: auto; + transition: opacity 0.2s; +} + +.hotspot:hover { + opacity: 0.8; +} + +/* UI 覆盖层 */ +#ui-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10; + font-family: 'Courier New', monospace; + font-weight: bold; +} + +/* 左上角 UI */ +#top-left-ui { + position: absolute; + top: 2vh; + left: 2vw; + color: white; + text-shadow: 2px 2px 4px #000; +} + +#time-display { + font-size: 2.5vw; + margin-bottom: 0.5vh; +} + +#night-display { + font-size: 1.5vw; + color: #ccc; +} + +/* 右下角 UI */ +#bottom-right-ui { + position: absolute; + bottom: 2vh; + right: 2vw; + color: white; + text-shadow: 2px 2px 4px #000; +} + +#oxygen-display { + display: flex; + align-items: center; + gap: 0; + font-size: 2vw; + font-weight: bold; +} + +#oxygen-display .vent-icon { + margin-right: 1vw; +} + +#power-value { + margin: 0; +} + +.percent-sign { + margin-left: 0.1vw; + margin-right: 0; +} + +.oxygen-unit { + margin-left: 1.2vw; + display: inline-flex; + align-items: baseline; +} + +#oxygen-display sub { + font-size: 1vw; + vertical-align: sub; + line-height: 0; +} + +.vent-icon { + width: 3vw; + height: 3vw; + object-fit: contain; + transition: none; + /* 禁用默认过渡,使用animation */ + animation: spin-fast 0.333s linear infinite; + /* 默认快速旋转(通风口开启)- 1秒3圈 */ +} + +/* 风扇旋转动画 - 快速(通风口开启) */ +@keyframes spin-fast { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +/* 风扇旋转动画 - 慢速(减速中) */ +@keyframes spin-slow { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +/* 风扇停止状态 */ +.vent-icon.stopped { + animation: none; +} + +/* 风扇减速状态 */ +.vent-icon.slowing { + animation: spin-slow 3s linear infinite; +} + +/* 风扇加速状态 */ +.vent-icon.speeding-up { + animation: spin-slow 3s linear infinite; +} + +animation: spin 0.8s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +/* 游戏按钮 */ +.game-button { + position: absolute; + width: 8vw; + height: 8vh; + background: rgba(255, 0, 0, 0.1); + border: 2px solid rgba(255, 0, 0, 0.3); + cursor: pointer; + transition: all 0.3s; + pointer-events: auto; + display: flex; + align-items: center; + justify-content: center; +} + +.game-button:hover { + background: rgba(255, 0, 0, 0.3); + border-color: rgba(255, 0, 0, 0.8); + box-shadow: 0 0 20px rgba(255, 0, 0, 0.5); +} + +.game-button .button-hint { + opacity: 0; + color: white; + font-size: 1.2vw; + font-weight: bold; + text-shadow: 2px 2px 4px #000; + transition: opacity 0.3s; + pointer-events: none; +} + +.game-button:hover .button-hint { + opacity: 1; +} + +/* 左下角:控制面板按钮 */ +#control-panel-btn { + bottom: 2vh; + left: 2vw; +} + +/* 右侧中间:摄像头按钮 */ +#camera-btn { + right: 0; + top: 50%; + transform: translateY(-50%); + height: 15vh; + width: 5vw; +} + +/* 摄像头面板 - 从右往左滑入 + 缩放效果 */ +#camera-panel { + position: absolute; + top: 5vh; + right: 7vw; + width: 70vw; + height: calc(70vw * 0.75); + /* 保持4:3宽高比 */ + max-height: 80vh; + /* 但不超过80vh */ + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-color: #000; + z-index: 20; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-end; + padding: 0; + transform: translateX(calc(100% + 7vw)) scale(0.5); + transform-origin: right center; + transition: transform 0.4s ease-in-out; + border: 3px solid #444; + box-shadow: -5px 0 20px rgba(0, 0, 0, 0.7); + opacity: 0; + overflow: hidden; + /* 防止EP图片溢出到摄像头面板外 */ +} + +#camera-panel.show { + transform: translateX(0) scale(1); + opacity: 1; +} + +#camera-panel.closing { + transform: translateX(calc(100% + 7vw)) scale(0.5); + opacity: 0; +} + +#camera-panel.transitioning { + background-image: none !important; +} + +/* 当前摄像头标签 */ +#current-cam-label { + position: absolute; + top: 15px; + left: 15px; + font-size: 24px; + font-weight: bold; + color: #fff; + font-family: Arial, sans-serif; + text-shadow: 2px 2px 4px #000; + z-index: 1; +} + +/* 角色图层容器 */ +#character-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; +} + +#character-overlay img { + position: absolute; + max-width: 100%; + max-height: 100%; + object-fit: contain; + opacity: 0; + transition: opacity 0.3s; +} + +#character-overlay img.visible { + opacity: 1; +} + +/* 摄像头切换静态噪点 */ +#camera-static-video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + pointer-events: none; + z-index: 2; + opacity: 0; + transition: opacity 0.2s; + display: none; +} + +#camera-static-video.active { + display: block; + opacity: 0.8; +} + +/* 摄像头故障标识 */ +#camera-error-label { + position: absolute; + top: 15px; + right: 25px; + font-size: 48px; + font-weight: bold; + color: #f00; + font-family: Arial, sans-serif; + text-shadow: 2px 2px 4px #000; + z-index: 3; + display: none; +} + +#camera-error-label.active { + display: block; + /* 移除闪烁动画 */ +} + +/* 地图容器 - 移到左下角 */ +#camera-grid { + position: absolute; + bottom: 15px; + left: 15px; + width: 28vw; + max-width: 380px; + z-index: 1; +} + +.camera-hotspot { + position: absolute; + cursor: pointer; + border-radius: 4px; + transition: all 0.2s; +} + +.camera-hotspot:hover { + transform: scale(1.05); +} + +/* 选中的摄像头绿色闪烁动画 */ +.camera-selected { + animation: greenPulse 1s ease-in-out infinite; +} + +@keyframes greenPulse { + + 0%, + 100% { + background: rgba(0, 255, 0, 0.6); + } + + 50% { + background: rgba(0, 255, 0, 0.2); + } +} + +/* ELECTROCUTE 按钮(霍金电击) - 只在cam6显示 */ +#shock-hawking-btn { + position: absolute; + bottom: 100px; + right: 25px; + width: 280px; + padding: 20px 0; + font-size: 24px; + background: rgba(0, 0, 0, 0.7); + border: 3px solid #fff; + color: #fff; + cursor: pointer; + transition: all 0.3s; + font-family: Arial, sans-serif; + font-weight: bold; + letter-spacing: 4px; + z-index: 1; + text-align: center; + display: none; + /* 默认隐藏 */ + box-sizing: border-box; +} + +#shock-hawking-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); +} + +#shock-hawking-btn:active { + transform: scale(0.95); +} + +/* PLAY SOUND 按钮 */ +#play-sound-btn { + position: absolute; + bottom: 25px; + right: 25px; + width: 280px; + padding: 20px 0; + font-size: 24px; + background: rgba(0, 0, 0, 0.7); + border: 3px solid #fff; + color: #fff; + cursor: pointer; + transition: all 0.3s; + font-family: Arial, sans-serif; + font-weight: bold; + letter-spacing: 4px; + z-index: 1; + text-align: center; + box-sizing: border-box; +} + +#play-sound-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); +} + +/* 摄像头关闭按钮 - 手机端显示 */ +#close-camera { + display: none; +} + +/* 主菜单 */ +#main-menu { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: url('/assets/images/menubackground.png') center/cover; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding-left: 5%; + z-index: 100; +} + +#main-menu>h1 { + margin-top: -10vh; + /* 整体上移 */ +} + +/* 星星图标 - 放在中间偏上位置 */ +#star-icon { + position: absolute; + top: 15vh; + left: 50%; + transform: translateX(-50%); + width: 5vw; + max-width: 80px; + height: auto; + z-index: 103; + animation: star-glow 2s ease-in-out infinite; +} + +/* 第二颗星星 - 在第一颗下面 */ +#star-icon-2 { + position: absolute; + top: 22vh; + /* 比第一颗低7vh */ + left: 50%; + transform: translateX(-50%); + width: 5vw; + max-width: 80px; + height: auto; + z-index: 103; + animation: star-glow 2s ease-in-out infinite 0.5s; + /* 延迟0.5s,产生交替闪烁效果 */ +} + +/* 第三颗星星 - 在第二颗下面 (20/20/20通关) */ +#star-icon-3 { + position: absolute; + top: 29vh; + /* 比第二颗低7vh */ + left: 50%; + transform: translateX(-50%); + width: 5vw; + max-width: 80px; + height: auto; + z-index: 103; + animation: star-glow 2s ease-in-out infinite 1s; + /* 延迟1s,产生交替闪烁效果 */ +} + +@keyframes star-glow { + + 0%, + 100% { + filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.8)); + transform: translateX(-50%) scale(1); + } + + 50% { + filter: drop-shadow(0 0 20px rgba(255, 215, 0, 1)); + transform: translateX(-50%) scale(1.05); + } +} + +/* Golden 霍金闪烁动画 */ +@keyframes golden-flicker { + 0% { + opacity: 0; + } + + 10% { + opacity: 1; + } + + 20% { + opacity: 0; + } + + 30% { + opacity: 1; + } + + 40% { + opacity: 0.5; + } + + 50% { + opacity: 1; + } + + 60% { + opacity: 0.8; + } + + 70% { + opacity: 1; + } + + 80% { + opacity: 0.6; + } + + 90% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +/* 静态雪花效果 - 使用 canvas */ +#static-canvas { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 101; + opacity: 0.5; + /* 增强透明度,从 0.25 改成 0.5 */ + mix-blend-mode: screen; + /* 改用 screen 模式,更明显 */ +} + +#main-menu>h1, +#main-menu>button, +#main-menu>img { + position: relative; + z-index: 102; +} + +#main-menu h1 { + font-size: 5vw; + margin-bottom: 5vh; + text-shadow: 4px 4px 8px rgba(0, 0, 0, 0.8); + color: #fff; + font-family: 'Courier New', monospace; + font-weight: bold; + letter-spacing: 2px; + line-height: 1.2; +} + +/* 音量按钮 */ +#volume-btn { + position: fixed; + top: 2vh; + right: 2vw; + padding: 15px 25px; + background: transparent; + border: none; + cursor: pointer; + z-index: 102; + /* 在白噪声(101)上面,但低于其他UI(103) */ + transition: all 0.2s; + font-family: 'Courier New', monospace; + font-size: 18px; + font-weight: bold; + color: #fff; + letter-spacing: 3px; + text-shadow: 4px 4px 8px rgba(0, 0, 0, 0.8); +} + +#volume-btn:hover { + transform: scale(1.05); + text-shadow: 4px 4px 12px rgba(0, 0, 0, 1); +} + +/* 音量设置面板 */ +#volume-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.98); + border: 2px solid #666; + border-radius: 0; + padding: 20px; + z-index: 105; + /* 面板要在最上面 */ + min-width: 400px; + max-width: 90vw; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.9), inset 0 0 20px rgba(0, 0, 0, 0.5); +} + +#volume-panel h3 { + color: #ff0000; + font-family: 'Courier New', monospace; + font-size: 18px; + margin-bottom: 15px; + margin-top: 0; + text-align: center; + letter-spacing: 3px; + text-shadow: + 0 0 10px #ff0000, + 0 0 20px #ff0000, + 2px 2px 4px #000; +} + +.volume-item { + margin-bottom: 15px; +} + +.volume-item label { + color: #ff0000; + font-family: 'Courier New', monospace; + font-size: 12px; + display: block; + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 1px; + text-shadow: + 0 0 5px #ff0000, + 0 0 10px #ff0000; +} + +.volume-slider-container { + display: flex; + align-items: center; + gap: 10px; +} + +.volume-slider-container input[type="range"] { + flex: 1; + height: 3px; + background: #333; + outline: none; + border-radius: 0; + cursor: pointer; + border: 1px solid #555; +} + +.volume-slider-container input[type="range"]::-webkit-slider-thumb { + appearance: none; + width: 12px; + height: 12px; + background: #666; + border-radius: 0; + cursor: pointer; + border: 1px solid #888; +} + +.volume-slider-container input[type="range"]::-moz-range-thumb { + width: 12px; + height: 12px; + background: #666; + border-radius: 0; + cursor: pointer; + border: 1px solid #888; +} + +.volume-percent { + color: #777; + font-family: 'Courier New', monospace; + font-size: 13px; + min-width: 45px; + text-align: right; +} + +#close-volume-panel { + width: 100%; + padding: 8px; + margin-top: 10px; + background: #222; + color: #888; + border: 1px solid #555; + border-radius: 0; + font-family: 'Courier New', monospace; + font-size: 13px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + letter-spacing: 2px; +} + +#close-volume-panel:hover { + background: #333; + color: #aaa; + border-color: #777; +} + +#main-menu button { + width: 20vw; + min-width: 200px; + padding: 1.5vh 2vw; + margin: 1.5vh 0; + font-size: 1.8vw; + background: transparent; + border: none; + color: #fff; + cursor: pointer; + transition: all 0.2s; + text-align: left; + font-family: 'Courier New', monospace; + font-weight: bold; + letter-spacing: 3px; + white-space: nowrap; +} + +#main-menu button::before { + content: '> '; + opacity: 0; + transition: opacity 0.2s; +} + +#main-menu button:hover::before, +#main-menu button:focus::before { + opacity: 1; +} + +#main-menu button:hover { + transform: translateX(10px); + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); +} + +/* 特殊夜晚按钮 */ +#special-night-btn { + color: #ffd700; + text-shadow: 0 0 10px rgba(255, 215, 0, 0.8); +} + +/* 恐怖脸闪烁效果 - 不再需要,改用背景图切换 */ +.copyright { + position: absolute; + bottom: 2vh; + left: 2vw; + font-size: 1.2vw; + color: #888; + font-family: 'Courier New', monospace; + z-index: 102; +} + +.reset-hint { + position: absolute; + bottom: 2vh; + left: 50%; + transform: translateX(-50%); + font-size: 0.8vw; + color: #888; + font-family: 'Courier New', monospace; + z-index: 102; +} + +.version { + position: absolute; + bottom: 2vh; + right: 8vw; + font-size: 1.8vw; + color: #ccc; + font-family: 'Courier New', monospace; + z-index: 102; + font-weight: bold; +} + +/* 游戏结束 */ +#game-over { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 100000; + overflow: hidden; +} + +/* 雪花视频背景 */ +#game-over-static { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 1; + opacity: 0.8; +} + +/* 游戏结束内容层 */ +#game-over-content { + position: relative; + z-index: 2; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +#game-over h2 { + font-size: 8vw; + margin-bottom: 2vh; + color: #ff0000; + text-shadow: + 0 0 10px #ff0000, + 0 0 20px #ff0000, + 0 0 30px #ff0000, + 3px 3px 6px #000; + font-weight: bold; + letter-spacing: 0.1em; +} + +#game-over-subtitle { + font-size: 1.8vw; + margin-bottom: 4vh; + color: #ff0000; + font-style: italic; + text-align: center; + padding: 0 2vw; + text-shadow: + 0 0 5px #ff0000, + 0 0 10px #ff0000, + 2px 2px 4px #000; + font-weight: bold; +} + +#game-over button { + width: 20vw; + min-width: 200px; + padding: 1.5vh 2vw; + margin: 1vh 0; + font-size: 1.5vw; + background: #333; + border: 2px solid #666; + color: #fff; + cursor: pointer; + transition: all 0.3s; +} + +#game-over button:hover { + background: #555; + border-color: #fff; +} + +/* 游戏教程提示 */ +#tutorial-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; +} + +#tutorial-content { + background: rgba(30, 30, 30, 0.95); + border: 3px solid #666; + padding: 4vh 5vw; + max-width: 60vw; + text-align: center; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.8); +} + +#tutorial-content h2 { + font-size: 3vw; + margin-bottom: 3vh; + color: #fff; + text-shadow: 2px 2px 4px #000; + letter-spacing: 0.1em; +} + +#tutorial-content p { + font-size: 1.4vw; + line-height: 1.8; + color: #ddd; + margin-bottom: 3vh; + text-align: left; +} + +#tutorial-got-it { + width: 15vw; + min-width: 150px; + padding: 1.5vh 3vw; + font-size: 1.8vw; + background: #555; + border: 2px solid #888; + color: #fff; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; + letter-spacing: 0.2em; +} + +#tutorial-got-it:hover { + background: #777; + border-color: #fff; + transform: scale(1.05); +} + +/* 隐藏类 */ +.hidden { + display: none !important; +} + + +/* 闪烁效果 */ +@keyframes flicker { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.3; + } +} + +.flicker { + animation: flicker 0.1s infinite; +} + +/* 警告闪烁动画 */ +@keyframes flash { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.3; + } +} + +/* 加载点动画 */ +@keyframes loadingDots { + + 0%, + 20% { + content: '.'; + } + + 40% { + content: '..'; + } + + 60%, + 100% { + content: '...'; + } +} + +.loading-dots { + animation: loadingDots 1s infinite; +} + +/* 过场动画 */ +#cutscene { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #000; + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + cursor: pointer; + opacity: 0; + transition: opacity 3s ease-in-out; +} + +#cutscene.fade-in { + opacity: 1; +} + +#cutscene.fade-out { + opacity: 0; +} + +#cutscene img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.cutscene-hint { + position: absolute; + bottom: 5vh; + left: 50%; + transform: translateX(-50%); + color: #fff; + font-size: 1.5vw; + font-family: 'Courier New', monospace; + animation: pulse 1.5s infinite; +} + +/* 每晚开始场景 */ +#night-intro { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #000; + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + opacity: 0; + transition: opacity 1.5s ease-in-out; +} + +#night-intro.fade-in { + opacity: 1; +} + +#night-intro.fade-out { + opacity: 0; +} + +#night-intro-text { + font-size: 8vw; + color: #fff; + font-family: 'Courier New', monospace; + font-weight: bold; + letter-spacing: 0.5vw; + text-shadow: 0 0 20px rgba(255, 255, 255, 0.5); + z-index: 1; +} + +/* 电眼闪烁动画 (Night 6) */ +@keyframes lightning-flicker { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.85; + } +} + +@keyframes lightning-pulse { + + 0%, + 100% { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + } + + 50% { + transform: translate(-50%, -50%) scale(1.1); + opacity: 0.9; + } +} + +@keyframes lightning-bolt { + + 0%, + 100% { + opacity: 0; + transform: translate(-50%, -50%) rotate(var(--rotation)) scaleY(0.5); + } + + 10%, + 20% { + opacity: 1; + transform: translate(-50%, -50%) rotate(var(--rotation)) scaleY(1); + } + + 30% { + opacity: 0; + transform: translate(-50%, -50%) rotate(var(--rotation)) scaleY(0.8); + } +} + +.lightning-eye-effect { + animation: lightning-flicker 0.1s infinite; +} + +/* Mobile Portrait */ +@media (max-width: 768px) and (orientation: portrait) { + + /* Preloader mobile styles */ + .preloader-logo { + font-size: 6vw; + } + + .preloader-text { + font-size: 3.5vw; + } + + .preloader-percentage { + font-size: 3vw; + } + + /* Increase touch target sizes */ + .control-panel-button, + .camera-button { + min-width: 60px !important; + min-height: 60px !important; + } + + /* Adjust UI text sizes for mobile */ + #top-left-ui { + top: 1vh; + left: 2vw; + } + + #time-display { + font-size: 4vw; + } + + #night-display { + font-size: 3vw; + } + + #bottom-right-ui { + bottom: 1vh; + right: 2vw; + } + + #oxygen-display { + font-size: 4vw; + } + + .vent-icon { + width: 5vw; + height: 5vw; + } + + /* Camera panel adjustments */ + #camera-panel { + width: 90vw; + height: 70vh; + right: 5vw; + top: 10vh; + } + + #current-cam-label { + font-size: 18px; + top: 10px; + left: 10px; + } + + #camera-error-label { + font-size: 36px; + top: 10px; + right: 15px; + } + + #camera-grid { + width: 40vw; + max-width: none; + bottom: 10px; + left: 10px; + } + + #play-sound-btn { + width: 180px; + padding: 10px 0; + font-size: 14px; + bottom: 15px; + right: 15px; + letter-spacing: 2px; + } + + #shock-hawking-btn { + width: 180px; + padding: 10px 0; + font-size: 14px; + bottom: 70px; + right: 15px; + letter-spacing: 2px; + } + + /* Control panel popup */ + #control-panel-popup { + width: 85vw !important; + left: 7.5vw !important; + top: 15vh !important; + min-height: 50vh !important; + font-size: 3.5vw !important; + } + + #control-panel-popup h3 { + font-size: 4vw !important; + } + + .control-row input { + font-size: 3vw !important; + } + + /* Main menu - 修复移动端按钮点击问题 */ + #main-menu { + padding-left: 8%; + padding-right: 8%; + align-items: center; + } + + #star-icon { + width: 12vw; + max-width: 60px; + top: 10vh; + } + + #star-icon-2 { + width: 12vw; + max-width: 60px; + top: 18vh; + /* 比第一颗低8vh */ + } + + #star-icon-3 { + width: 12vw; + max-width: 60px; + top: 26vh; + /* 比第二颗低8vh */ + } + + #main-menu h1 { + font-size: 10vw; + margin-bottom: 8vh; + text-align: center; + width: 100%; + } + + #main-menu button { + font-size: 5vw; + width: 70vw; + max-width: 400px; + padding: 3vh 6vw; + margin: 2vh 0; + text-align: center; + min-height: 60px; + touch-action: manipulation; + -webkit-tap-highlight-color: rgba(255, 255, 255, 0.2); + position: relative; + z-index: 103; + } + + #main-menu button::before { + content: ''; + opacity: 0; + } + + #main-menu button:hover::before, + #main-menu button:focus::before { + opacity: 0; + } + + #main-menu button:active { + background: rgba(255, 255, 255, 0.1); + transform: scale(0.98); + } + + .copyright { + font-size: 2.5vw; + left: 50%; + transform: translateX(-50%); + text-align: center; + width: 90%; + } + + .reset-hint { + font-size: 2.5vw; + } + + .version { + font-size: 2.5vw; + } + + /* Game over screen */ + #game-over h2 { + font-size: 12vw; + } + + #game-over-subtitle { + font-size: 3.5vw; + } + + #game-over button { + font-size: 4vw; + width: 60vw; + padding: 2vh 4vw; + } + + /* Tutorial */ + #tutorial-content { + max-width: 85vw; + padding: 3vh 4vw; + } + + #tutorial-content h2 { + font-size: 5vw; + } + + #tutorial-content p { + font-size: 3.5vw; + } + + #tutorial-got-it { + font-size: 4vw; + width: 40vw; + padding: 2vh 4vw; + } + + /* Night intro */ + #night-intro-text { + font-size: 12vw; + } + + .cutscene-hint { + font-size: 3.5vw; + } +} + +/* Mobile Landscape - 横屏优化 */ +@media (max-width: 926px) and (orientation: landscape) { + + /* Preloader mobile styles */ + .preloader-logo { + font-size: 4vh; + } + + .preloader-text { + font-size: 2vh; + } + + .preloader-percentage { + font-size: 2vh; + } + + /* 横屏时增大摄像头按钮并确保可见 */ + #camera-btn { + width: 70px !important; + height: 60vh !important; + min-width: 70px !important; + min-height: 200px !important; + background: rgba(0, 0, 0, 0.95) !important; + border: 3px solid rgba(255, 255, 255, 0.7) !important; + border-right: none !important; + z-index: 25 !important; + top: 30vh !important; + transform: translateY(0) !important; + } + + #camera-btn .camera-arrow { + font-size: 20px !important; + } + + #camera-btn div[style*="CAMERA"] { + font-size: 14px !important; + } + + /* UI adjustments for landscape */ + #top-left-ui { + top: 1vh; + left: 1vw; + } + + #time-display { + font-size: 3vh; + } + + #night-display { + font-size: 2vh; + } + + #bottom-right-ui { + bottom: 1vh; + right: 1vw; + } + + #oxygen-display { + font-size: 3vh; + } + + .vent-icon { + width: 4vh; + height: 4vh; + } + + /* Camera panel - 横屏时不占满屏幕,留出右侧空间 */ + #camera-panel { + width: calc(100vw - 70px); + height: 100vh; + right: 70px; + top: 0; + border: none; + border-right: 3px solid #444; + } + + #current-cam-label { + font-size: 2.5vh; + top: 1vh; + left: 1vw; + } + + #camera-error-label { + font-size: 5vh; + top: 1vh; + right: 2vw; + } + + /* Camera grid - 横屏时缩小并移到左下角 */ + #camera-grid { + width: 25vw; + max-width: 280px; + bottom: 1vh; + left: 1vw; + } + + /* 横屏时调整按钮布局 */ + #play-sound-btn { + width: 180px; + padding: 2vh 0; + font-size: 2.5vh; + bottom: 2vh; + right: 2vw; + letter-spacing: 2px; + } + + #shock-hawking-btn { + width: 180px; + padding: 2vh 0; + font-size: 2.5vh; + bottom: 10vh; + right: 2vw; + letter-spacing: 2px; + } + + /* Main menu landscape */ + #main-menu { + padding-left: 5%; + align-items: flex-start; + } + + #main-menu h1 { + font-size: 6vh; + margin-bottom: 3vh; + text-align: left; + } + + #main-menu button { + font-size: 2.5vh; + width: 30vw; + min-width: 200px; + padding: 1.5vh 3vw; + margin: 1vh 0; + min-height: 50px; + } + + .copyright, + .reset-hint, + .version { + font-size: 1.5vh; + } + + /* Game over screen landscape */ + #game-over h2 { + font-size: 8vh; + } + + #game-over-subtitle { + font-size: 2vh; + } + + #game-over button { + font-size: 2.5vh; + width: 30vw; + padding: 1.5vh 3vw; + min-height: 50px; + } + + /* Tutorial - 横屏时大幅缩小 */ + #tutorial-content { + max-width: 70vw; + max-height: 85vh; + padding: 2vh 3vw; + overflow-y: auto; + } + + #tutorial-content h2 { + font-size: 2.5vh; + margin-bottom: 1.5vh; + } + + #tutorial-content p { + font-size: 1.6vh; + line-height: 1.5; + margin-bottom: 2vh; + } + + #tutorial-got-it { + font-size: 2vh; + width: 20vw; + min-width: 120px; + padding: 1.5vh 3vw; + min-height: 45px; + } + + /* Night intro landscape */ + #night-intro-text { + font-size: 8vh; + } + + .cutscene-hint { + font-size: 2vh; + } + + /* Volume button landscape */ + #volume-btn { + font-size: 2vh; + padding: 1vh 2vw; + top: 1vh; + right: 1vw; + } + + /* Volume panel landscape - 优化高度防止溢出 */ + #volume-panel { + padding: 2vh 3vw; + min-width: 300px; + max-width: 70vw; + max-height: 85vh; + } + + #volume-panel h3 { + font-size: 2vh; + margin-bottom: 1.5vh; + } + + .volume-item { + margin-bottom: 1.5vh; + } + + .volume-item label { + font-size: 1.5vh; + margin-bottom: 0.5vh; + } + + .volume-percent { + font-size: 1.5vh; + } + + #close-volume-panel { + font-size: 1.8vh; + padding: 1vh; + margin-top: 1.5vh; + } +} + +/* Prevent text selection on touch devices */ +@media (hover: none) and (pointer: coarse) { + * { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; + } + + input, + textarea { + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; + } +} + +/* Touch feedback */ +@media (hover: none) and (pointer: coarse) { + + .hotspot:active, + .control-panel-button:active, + .camera-button:active, + button:active { + opacity: 0.7; + transform: scale(0.95); + } +} + + +/* ==================== Custom Night Menu ==================== */ +#custom-night-menu { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #000000; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 100; + padding: 2vh; +} + +#custom-night-menu h1 { + font-size: 5vw; + color: #fff; + margin-bottom: 4vh; + text-shadow: 0 0 20px rgba(255, 0, 0, 0.5); +} + +.custom-night-controls { + display: flex; + flex-direction: column; + gap: 3vh; + width: 60%; + max-width: 800px; + margin-bottom: 4vh; +} + +.ai-control { + display: flex; + align-items: center; + justify-content: space-between; + background: rgba(20, 20, 20, 0.8); + padding: 2vh 3vw; + border-radius: 10px; + border: 2px solid rgba(100, 100, 100, 0.3); + transition: all 0.3s; +} + +.ai-control:hover { + background: rgba(40, 40, 40, 0.9); + border-color: rgba(150, 150, 150, 0.5); +} + +.ai-name { + font-size: 2vw; + font-weight: bold; + color: #fff; + min-width: 150px; +} + +.ai-slider-container { + display: flex; + align-items: center; + gap: 1vw; + flex: 1; + margin: 0 2vw; +} + +.ai-btn-minus, +.ai-btn-plus { + width: 40px; + height: 40px; + font-size: 24px; + font-weight: bold; + background: rgba(50, 50, 50, 0.8); + border: 2px solid rgba(100, 100, 100, 0.5); + color: #fff; + cursor: pointer; + border-radius: 5px; + transition: all 0.2s; +} + +.ai-btn-minus:hover, +.ai-btn-plus:hover { + background: rgba(80, 80, 80, 0.9); + border-color: rgba(150, 150, 150, 0.7); + transform: scale(1.1); +} + +.ai-btn-minus:active, +.ai-btn-plus:active { + transform: scale(0.95); +} + +.ai-slider { + flex: 1; + height: 8px; + -webkit-appearance: none; + appearance: none; + background: rgba(80, 80, 80, 0.6); + border-radius: 5px; + outline: none; +} + +.ai-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 24px; + height: 24px; + background: #ddd; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 0 10px rgba(200, 200, 200, 0.5); + transition: all 0.2s; +} + +.ai-slider::-webkit-slider-thumb:hover { + transform: scale(1.2); + box-shadow: 0 0 15px rgba(220, 220, 220, 0.8); +} + +.ai-slider::-moz-range-thumb { + width: 24px; + height: 24px; + background: #ddd; + border-radius: 50%; + cursor: pointer; + border: none; + box-shadow: 0 0 10px rgba(200, 200, 200, 0.5); + transition: all 0.2s; +} + +.ai-slider::-moz-range-thumb:hover { + transform: scale(1.2); + box-shadow: 0 0 15px rgba(220, 220, 220, 0.8); +} + +.ai-value { + font-size: 2.5vw; + font-weight: bold; + color: #fff; + min-width: 60px; + text-align: center; + text-shadow: 0 0 10px rgba(255, 255, 255, 0.5); +} + +.custom-night-buttons { + display: flex; + gap: 2vw; +} + +.custom-night-buttons button { + padding: 1.5vh 3vw; + font-size: 1.5vw; + font-weight: bold; + background: rgba(50, 50, 50, 0.8); + border: 2px solid rgba(100, 100, 100, 0.5); + color: #fff; + cursor: pointer; + border-radius: 5px; + transition: all 0.3s; + min-width: 150px; +} + +.custom-night-buttons button:hover { + background: rgba(80, 80, 80, 0.9); + border-color: rgba(150, 150, 150, 0.7); + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); +} + +.custom-night-buttons button:active { + transform: translateY(0); +} + +/* Mobile 适配 */ +@media (max-width: 768px) { + #custom-night-menu h1 { + font-size: 8vw; + } + + .custom-night-controls { + width: 90%; + } + + .ai-control { + flex-direction: column; + gap: 1vh; + padding: 2vh; + } + + .ai-name { + font-size: 5vw; + min-width: auto; + } + + .ai-slider-container { + width: 100%; + margin: 0; + } + + .ai-value { + font-size: 6vw; + } + + .custom-night-buttons { + flex-direction: column; + width: 80%; + } + + .custom-night-buttons button { + font-size: 4vw; + width: 100%; + } +} \ No newline at end of file diff --git a/js/config.js b/js/config.js index 099dfc5b..5fceb1ac 100644 --- a/js/config.js +++ b/js/config.js @@ -1 +1 @@ -var json={"games":{"2048":{"path":"2048","aliases":[],"categories":[]},"1v1.LOL":{"path":"1v1-lol","aliases":[],"categories":["online","battle"]},"10 minutes till dawn":{"path":"10-minutes-till-dawn","aliases":[],"categories":[]},"Achievement Unlocked":{"path":"flash/?game=achievement-unlocked","aliases":[],"categories":[]},"Achievement Unlocked 2":{"path":"flash/?game=achievement-unlocked-2","aliases":[],"categories":[]},"Achievement Unlocked 3":{"path":"flash/?game=achievement-unlocked-3","aliases":[],"categories":[]},"A Dark Room":{"path":"a-dark-room","aliases":[],"categories":[]},"Abandoned":{"path":"abandoned","aliases":[],"categories":["escape"]},"Abandoned 2":{"path":"abandoned-2","aliases":[],"categories":[]},"Ages of Conflict":{"path":"ages-of-conflict","aliases":[],"categories":["escape"]},"Age of War":{"path":"flash/?game=age-of-war","aliases":[],"categories":[]},"Age of War Hacked":{"path":"flash/?game=age-of-war-hacked","aliases":[],"categories":[]},"Aground":{"path":"aground","aliases":[],"categories":[]},"Amidst The Sky":{"path":"amidst-the-sky","aliases":[],"categories":[]},"Another Gentlemans Adventure":{"path":"another-gentlemans-adventure","aliases":[],"categories":[]},"Awesome Tanks":{"path":"awesome-tanks","aliases":[],"categories":[]},"Awesome Tanks 2":{"path":"awesome-tanks-2","aliases":[],"categories":[]},"Babel Tower":{"path":"babel-tower","aliases":[],"categories":[]},"Basket Random":{"path":"basket-random","aliases":[],"categories":[]},"Basketball Stars":{"path":"basketball-stars","aliases":[],"categories":[]},"Bit Life":{"path":"bit-life","aliases":[],"categories":[]},"Boxing Random":{"path":"boxing-random","aliases":[],"categories":[]},"Bloons":{"path":"flash/?game=bloons","aliases":[],"categories":[]},"Bloons TD 1":{"path":"flash/?game=bloons-td-1","aliases":[],"categories":[]},"Bloons TD 2":{"path":"flash/?game=bloons-td-2","aliases":[],"categories":[]},"Bloons TD 3":{"path":"flash/?game=bloons-td-3","aliases":[],"categories":[]},"Bloons TD 4":{"path":"flash/?game=bloons-td-4","aliases":[],"categories":[]},"Bloons TD 5":{"path":"flash/?game=bloons-td-5","aliases":[],"categories":[]},"Bloxorz":{"path":"flash/?game=bloxorz","aliases":[],"categories":[]},"Brawl Stars Project Laser":{"path":"brawl-stars-project-laser","aliases":[],"categories":[]},"Breaklock":{"path":"breaklock","aliases":[],"categories":[]},"Cat Ninja":{"path":"flash/?game=cat-ninja","aliases":[],"categories":[]},"Curveball":{"path":"flash/?game=curveball","aliases":[],"categories":[]},"Chrome Dino":{"path":"chrome-dino","aliases":[],"categories":[]},"Clicker Heroes":{"path":"clicker-heroes","aliases":[],"categories":[]},"Clicker Heroes Updated":{"path":"clicker-heroes-updated","aliases":[],"categories":[]},"Cookie Clicker":{"path":"cookie-clicker","aliases":[],"categories":[]},"Core Ball":{"path":"core-ball","aliases":[],"categories":[]},"Crossy Road":{"path":"crossy-road","aliases":[],"categories":[]},"Conway's Game of Life":{"path":"conways-game-of-life","aliases":[],"categories":[]},"Creative Kill Chamber":{"path":"flash/?game=creative-kill-chamber","aliases":[],"categories":[]},"Cut The Rope":{"path":"cut-the-rope","aliases":[],"categories":[]},"Doge Miner":{"path":"doge-miner","aliases":[],"categories":[]},"Don't Escape":{"path":"flash/?game=dont-escape","aliases":[],"categories":[]},"Don't Escape 2":{"path":"flash/?game=dont-escape-2","aliases":[],"categories":[]},"Don't Escape 3":{"path":"flash/?game=dont-escape-3","aliases":[],"categories":[]},"Doodle Jump":{"path":"doodle-jump","aliases":[],"categories":[]},"Drift Boss":{"path":"drift-boss","aliases":[],"categories":[]},"Drift Hunters":{"path":"drift-hunters","aliases":[],"categories":[]},"Drive Mad":{"path":"drive-mad","aliases":[],"categories":[]},"Drive Mad S":{"path":"drive-mad-s","aliases":[],"categories":[]},"Duck Life":{"path":"flash/?game=duck-life","aliases":[],"categories":[]},"Duck Life 2":{"path":"flash/?game=duck-life-2","aliases":[],"categories":[]},"Duck Life 3":{"path":"flash/?game=duck-life-3","aliases":[],"categories":[]},"Duck Life 4":{"path":"duck-life-4","aliases":[],"categories":[]},"Duck Life Treasure Hunt":{"path":"flash/?game=duck-life-treasure-hunt","aliases":[],"categories":[]},"Dune!":{"path":"dune","aliases":[],"categories":[]},"Eaglercraft Ampler Launcher":{"path":"ampler-launcher","aliases":[],"categories":[]},"Eggy Car":{"path":"eggy-car","aliases":[],"categories":[]},"EvoWars":{"path":"evowars","aliases":[],"categories":[]},"Escape The Car":{"path":"flash/?game=escape-the-car","aliases":[],"categories":[]},"Escape The Closet":{"path":"flash/?game=escape-the-closet","aliases":[],"categories":[]},"Escape The Phonebooth":{"path":"flash/?game=escape-the-phonebooth","aliases":[],"categories":[]},"Escape The Bathroom":{"path":"flash/?game=escape-the-bathroom","aliases":[],"categories":[]},"Escape The Freezer":{"path":"flash/?game=escape-the-freezer","aliases":[],"categories":[]},"Escape The Shack":{"path":"flash/?game=escape-the-shack","aliases":[],"categories":[]},"Family Feud":{"path":"family-feud","aliases":[],"categories":[]},"Fireboy and Watergirl in the Forest Temple":{"path":"fireboy-and-watergirl","aliases":["Fireboy and Watergirl","Fireboy and Watergirl 1"],"categories":[]},"Fireboy and Watergirl in the Light Temple":{"path":"fireboy-and-watergirl-2","aliases":["Fireboy and Watergirl 2"],"categories":[]},"Fireboy and Watergirl in the Ice Temple":{"path":"fireboy-and-watergirl-3","aliases":["Fireboy and Watergirl 3"],"categories":[]},"Fireboy and Watergirl in the Crystal Temple":{"path":"fireboy-and-watergirl-4","aliases":["Fireboy and Watergirl 4"],"categories":[]},"Flash Chess":{"path":"flash/?game=flash-chess","aliases":[],"categories":[]},"Friday Night Funkin":{"path":"friday-night-funkin","aliases":[],"categories":[]},"Factory Balls":{"path":"flash/?game=factory-balls","aliases":[],"categories":[]},"Flappy Bird":{"path":"flappy-bird","aliases":[],"categories":[]},"Geometry Dash Lite":{"path":"geometry-dash-lite","aliases":[],"categories":[]},"Geometry Dash Remastered":{"path":"geometry-dash-remastered","aliases":[],"categories":[]},"Geometry Vibes":{"path":"geometry-vibes","aliases":[],"categories":[]},"Geometry Vibes Monster":{"path":"geometry-vibes-monster","aliases":[],"categories":[]},"Geometry Vibes X-Ball":{"path":"geometry-vibes-x-ball","aliases":[],"categories":[]},"Getaway Shootout":{"path":"getaway-shootout","aliases":[],"categories":[]},"Gons io":{"path":"gons-io","aliases":[],"categories":[]},"Gun Mayhem":{"path":"flash/?game=gun-mayhem","aliases":[],"categories":[]},"Gun Mayhem 2":{"path":"flash/?game=gun-mayhem-2","aliases":[],"categories":[]},"Gun Spin":{"path":"gunspin","aliases":[],"categories":[]},"Henry Stickmin 0: Breaking The Bank":{"path":"flash/?game=breaking-the-bank","aliases":[],"categories":[]},"Henry Stickmin 1: Escaping The Prison":{"path":"flash/?game=escaping-the-prison","aliases":[],"categories":[]},"Henry Stickmin 2: Stealing The Diamond":{"path":"flash/?game=stealing-the-diamond","aliases":[],"categories":[]},"Henry Stickmin 3: Infiltrating The Airship":{"path":"flash/?game=infiltrating-the-airship","aliases":[],"categories":[]},"Henry Stickmin 4: Fleeing the Complex":{"path":"flash/?game=fleeing-the-complex","aliases":[],"categories":[]},"Hextris":{"path":"hextris","aliases":[],"categories":[]},"Incremancer":{"path":"incremancer","aliases":[],"categories":[]},"Learn To Fly":{"path":"flash/?game=learn-to-fly","aliases":[],"categories":[]},"Learn To Fly 2":{"path":"flash/?game=learn-to-fly-2","aliases":[],"categories":[]},"Learn To Fly 3":{"path":"flash/?game=learn-to-fly-3","aliases":[],"categories":[]},"Learn To Fly Idle":{"path":"flash/?game=learn-to-fly-idle","aliases":[],"categories":[]},"Lion Solider's Vengeance":{"path":"lion-soldiers-vengeance","aliases":[],"cateogires":[]},"Idle Breakout":{"path":"idle-breakout","aliases":[],"categories":[]},"Maptroid":{"path":"maptroid","aliases":[],"categories":[]},"Mario":{"path":"mario-game","aliases":[],"categories":[]},"Monkey Mart":{"path":"monkey-mart","aliases":[],"categories":[]},"MotoX3M":{"path":"motox3m","aliases":[],"categories":[]},"N-gon":{"path":"n-gon","aliases":[],"categories":[]},"N Step Steve Part 1":{"path":"n-step-steve-part-1","aliases":[],"categories":[]},"N Step Steve Part 2":{"path":"n-step-steve-part-2","aliases":[],"categories":[]},"OvO":{"path":"ovo/1.4.4","aliases":[],"categories":[]},"OvO 2":{"path":"ovo/2.0.2alpha","aliases":[],"categories":[]},"Papas Pizzeria":{"path":"flash/?game=papas-pizzaria","aliases":[],"categories":[]},"Papas Freezeria":{"path":"flash/?game=papas-freezeria","aliases":[],"categories":[]},"Particle Clicker":{"path":"particle-clicker","aliases":[],"categories":[]},"Planet Life":{"path":"planet-life","aliases":[],"categories":[]},"P.craft":{"path":"pcraft","aliases":[],"categories":[]},"Progress Knight Quest":{"path":"progress-knight-quest","aliases":[],"categories":[]},"Progress Knight Reborn":{"path":"progress-knight-reborn","aliases":[],"categories":[]},"Pull Of War":{"path":"pull-of-war","aliases":[],"categories":[]},"Reach The Core":{"path":"reach-the-core","aliases":[],"categories":[]},"Restless Wing Syndrome":{"path":"restless-wing-syndrome","aliases":[],"categories":[]},"Raft Wars":{"path":"flash/?game=raft-wars","aliases":[],"categories":[]},"Raft Wars 2":{"path":"flash/?game=raft-wars-2","aliases":[],"categories":[]},"Retro Bowl":{"path":"retro-bowl","aliases":[],"categories":[]},"Retro Bowl Old":{"path":"retro-bowl-old","aliases":[],"categories":[]},"Rift Shift":{"path":"rift-shift","aliases":[],"categories":[]},"Rocket League 2D":{"path":"rocket-league-2d","aliases":[],"categories":[]},"Rogue Soul":{"path":"flash/?game=rogue-soul","aliases":[],"categories":[]},"Rogue Soul 2":{"path":"flash/?game=rogue-soul-2","aliases":[],"categories":[]},"Riddle School":{"path":"flash/?game=riddle-school","aliases":[],"categories":["escape"]},"Riddle School 2":{"path":"flash/?game=riddle-school-2","aliases":[],"categories":["escape"]},"Riddle School 3":{"path":"flash/?game=riddle-school-3","aliases":[],"categories":["escape"]},"Riddle School 4":{"path":"flash/?game=riddle-school-4","aliases":[],"categories":["escape"]},"Riddle School 5":{"path":"flash/?game=riddle-school-5","aliases":[],"categories":["escape"]},"Riddle Transfer":{"path":"flash/?game=riddle-transfer","aliases":[],"categories":["escape"]},"Riddle Transfer 2":{"path":"flash/?game=riddle-transfer-2","aliases":[],"categories":["escape"]},"Rooftop Snipers":{"path":"rooftop-snipers","aliases":[],"categories":[]},"Rookie Bowman":{"path":"rookie-bowman","aliases":[],"categories":[]},"Run 3":{"path":"run-3","aliases":[],"categories":[]},"Run 3 Beta (may not work)":{"path":"run-3-beta","aliases":[],"categories":[]},"Sabercut":{"path":"sabercut","aliases":[],"categories":[]},"Sandspiel":{"path":"sandspiel","aliases":[],"categories":[]},"Scuba Bear":{"path":"scuba-bear","aliases":[],"categories":[]},"Shadow Fight":{"path":"shadow-fight","aliases":[],"categories":[]},"Slice Master":{"path":"slice-master","aliases":[],"categories":[]},"Slope":{"path":"slope","aliases":[],"categories":[]},"Smash Karts":{"path":"smash-karts","aliases":[],"categories":[]},"Soccer Random":{"path":"soccer-random","aliases":[],"categories":[]},"Station Saturn":{"path":"station-saturn","aliases":[],"categories":[]},"Stickman Hook":{"path":"stickman-hook","aliases":[],"categories":[]},"Subway Surfers":{"path":"subway-surfers","aliases":[],"categories":[]},"Subway Surfers New York":{"path":"subway-surfers-ny","aliases":[],"categories":[]},"Stick RPG Complete":{"path":"flash/?game=stick-rpg-complete","aliases":[],"categories":[]},"Stick War":{"path":"flash/?game=stick-war","aliases":[],"categories":[]},"Submachine":{"path":"flash/?game=submachine","aliases":[],"categories":["escape"]},"Submachine 2":{"path":"flash/?game=submachine-2","aliases":[],"categories":["escape"]},"Submachine 3":{"path":"flash/?game=submachine-3","aliases":[],"categories":["escape"]},"Submachine 4":{"path":"flash/?game=submachine-4","aliases":[],"categories":["escape"]},"Submachine 5":{"path":"flash/?game=submachine-5","aliases":[],"categories":["escape"]},"Submachine 6":{"path":"flash/?game=submachine-6","aliases":[],"categories":["escape"]},"Submachine 7":{"path":"flash/?game=submachine-7","aliases":[],"categories":["escape"]},"Submachine 8":{"path":"flash/?game=submachine-8","aliases":[],"categories":["escape"]},"Submachine 9":{"path":"flash/?game=submachine-9","aliases":[],"categories":["escape"]},"Submachine 10":{"path":"flash/?game=submachine-10","aliases":[],"categories":["escape"]},"Submachine 0":{"path":"flash/?game=submachine-0","aliases":[],"categories":["escape"]},"Submachine 32 Chambers":{"path":"flash/?game=submachine-32-chambers","aliases":[],"categories":["escape"]},"Submachine FLF":{"path":"flash/?game=submachine-flf","aliases":[],"categories":["escape"]},"Sugar Sugar":{"path":"flash/?game=sugar-sugar","aliases":[],"categories":[]},"Swords and Souls":{"path":"flash/?game=swords-and-souls","aliases":[],"categories":[]},"The Final Earth":{"path":"the-final-earth","aliases":[],"categories":[]},"The Treasure":{"path":"the-treasure","aliases":[],"categories":["escape"]},"There Is No Game":{"path":"there-is-no-game","aliases":[],"categories":[]},"Tic Tac What?":{"path":"tic-tac-what","aliases":[],"categories":[]},"Time Shooter":{"path":"time-shooter","aliases":[],"categories":[]},"Time Shooter 3":{"path":"time-shooter-3","aliases":[],"categories":[]},"Tiny Fishing":{"path":"tiny-fishing","aliases":[],"categories":[]},"Trace":{"path":"trace","aliases":[],"categories":["escape"]},"Tanuki Sunset":{"path":"tanuki-sunset","aliases":[],"categories":[]},"Temple Run 2":{"path":"temple-run-2","aliases":[],"categories":[]},"The Impossible Quiz":{"path":"flash/?game=the-impossible-quiz","aliases":[],"categories":[]},"This Is The Only Level":{"path":"flash/?game=this-is-the-only-level","aliases":[],"categories":[]},"This Is The Only Level 2":{"path":"flash/?game=this-is-the-only-level-2","aliases":[],"categories":[]},"Tunnel Rush":{"path":"tunnel-rush","aliases":[],"categories":[]},"Two Ball 3d":{"path":"two-ball-3d","aliases":[],"categories":[]},"Vex 3":{"path":"vex-3","aliases":[],"categories":[]},"Vex 4":{"path":"vex-4","aliases":[],"categories":[]},"Vex 5":{"path":"vex-5","aliases":[],"categories":[]},"Vex 6":{"path":"vex-6","aliases":[],"categories":[]},"Vex 7":{"path":"vex-7","aliases":[],"categories":[]},"Volley Random":{"path":"volley-random","aliases":[],"categories":[]},"Web OSU":{"path":"web-osu","aliases":[],"categories":[]},"Ultimate Chess":{"path":"flash/?game=ultimate-chess","aliases":[],"categories":[]},"X Trench Run":{"path":"x-trench-run","aliases":[],"categories":[]},"Yohoho":{"path":"yohoho","aliases":[],"categories":[]}},"themes":{},"config":{"proxy":true,"proxyPath":"https://monkey.nordparrot.ro"}} \ No newline at end of file +var json={"games":{"2048":{"path":"2048","aliases":[],"categories":[]},"1v1.LOL":{"path":"1v1-lol","aliases":[],"categories":["online","battle"]},"10 minutes till dawn":{"path":"10-minutes-till-dawn","aliases":[],"categories":[]},"Achievement Unlocked":{"path":"flash/?game=achievement-unlocked","aliases":[],"categories":[]},"Achievement Unlocked 2":{"path":"flash/?game=achievement-unlocked-2","aliases":[],"categories":[]},"Achievement Unlocked 3":{"path":"flash/?game=achievement-unlocked-3","aliases":[],"categories":[]},"A Dark Room":{"path":"a-dark-room","aliases":[],"categories":[]},"Abandoned":{"path":"abandoned","aliases":[],"categories":["escape"]},"Abandoned 2":{"path":"abandoned-2","aliases":[],"categories":[]},"Ages of Conflict":{"path":"ages-of-conflict","aliases":[],"categories":["escape"]},"Age of War":{"path":"flash/?game=age-of-war","aliases":[],"categories":[]},"Age of War Hacked":{"path":"flash/?game=age-of-war-hacked","aliases":[],"categories":[]},"Aground":{"path":"aground","aliases":[],"categories":[]},"Amidst The Sky":{"path":"amidst-the-sky","aliases":[],"categories":[]},"Another Gentlemans Adventure":{"path":"another-gentlemans-adventure","aliases":[],"categories":[]},"Awesome Tanks":{"path":"awesome-tanks","aliases":[],"categories":[]},"Awesome Tanks 2":{"path":"awesome-tanks-2","aliases":[],"categories":[]},"Babel Tower":{"path":"babel-tower","aliases":[],"categories":[]},"Basket Random":{"path":"basket-random","aliases":[],"categories":[]},"Basketball Stars":{"path":"basketball-stars","aliases":[],"categories":[]},"Bit Life":{"path":"bit-life","aliases":[],"categories":[]},"Boxing Random":{"path":"boxing-random","aliases":[],"categories":[]},"Bloons":{"path":"flash/?game=bloons","aliases":[],"categories":[]},"Bloons TD 1":{"path":"flash/?game=bloons-td-1","aliases":[],"categories":[]},"Bloons TD 2":{"path":"flash/?game=bloons-td-2","aliases":[],"categories":[]},"Bloons TD 3":{"path":"flash/?game=bloons-td-3","aliases":[],"categories":[]},"Bloons TD 4":{"path":"flash/?game=bloons-td-4","aliases":[],"categories":[]},"Bloons TD 5":{"path":"flash/?game=bloons-td-5","aliases":[],"categories":[]},"Bloxorz":{"path":"flash/?game=bloxorz","aliases":[],"categories":[]},"Brawl Stars Project Laser":{"path":"brawl-stars-project-laser","aliases":[],"categories":[]},"Breaklock":{"path":"breaklock","aliases":[],"categories":[]},"Cat Ninja":{"path":"flash/?game=cat-ninja","aliases":[],"categories":[]},"Curveball":{"path":"flash/?game=curveball","aliases":[],"categories":[]},"Chrome Dino":{"path":"chrome-dino","aliases":[],"categories":[]},"Clicker Heroes":{"path":"clicker-heroes","aliases":[],"categories":[]},"Clicker Heroes Updated":{"path":"clicker-heroes-updated","aliases":[],"categories":[]},"Cookie Clicker":{"path":"cookie-clicker","aliases":[],"categories":[]},"Core Ball":{"path":"core-ball","aliases":[],"categories":[]},"Crossy Road":{"path":"crossy-road","aliases":[],"categories":[]},"Conway's Game of Life":{"path":"conways-game-of-life","aliases":[],"categories":[]},"Creative Kill Chamber":{"path":"flash/?game=creative-kill-chamber","aliases":[],"categories":[]},"Cut The Rope":{"path":"cut-the-rope","aliases":[],"categories":[]},"Doge Miner":{"path":"doge-miner","aliases":[],"categories":[]},"Don't Escape":{"path":"flash/?game=dont-escape","aliases":[],"categories":[]},"Don't Escape 2":{"path":"flash/?game=dont-escape-2","aliases":[],"categories":[]},"Don't Escape 3":{"path":"flash/?game=dont-escape-3","aliases":[],"categories":[]},"Doodle Jump":{"path":"doodle-jump","aliases":[],"categories":[]},"Drift Boss":{"path":"drift-boss","aliases":[],"categories":[]},"Drift Hunters":{"path":"drift-hunters","aliases":[],"categories":[]},"Drive Mad":{"path":"drive-mad","aliases":[],"categories":[]},"Drive Mad S":{"path":"drive-mad-s","aliases":[],"categories":[]},"Duck Life":{"path":"flash/?game=duck-life","aliases":[],"categories":[]},"Duck Life 2":{"path":"flash/?game=duck-life-2","aliases":[],"categories":[]},"Duck Life 3":{"path":"flash/?game=duck-life-3","aliases":[],"categories":[]},"Duck Life 4":{"path":"duck-life-4","aliases":[],"categories":[]},"Duck Life Treasure Hunt":{"path":"flash/?game=duck-life-treasure-hunt","aliases":[],"categories":[]},"Dune!":{"path":"dune","aliases":[],"categories":[]},"Eaglercraft Ampler Launcher":{"path":"ampler-launcher","aliases":[],"categories":[]},"Eggy Car":{"path":"eggy-car","aliases":[],"categories":[]},"EvoWars":{"path":"evowars","aliases":[],"categories":[]},"Escape The Car":{"path":"flash/?game=escape-the-car","aliases":[],"categories":[]},"Escape The Closet":{"path":"flash/?game=escape-the-closet","aliases":[],"categories":[]},"Escape The Phonebooth":{"path":"flash/?game=escape-the-phonebooth","aliases":[],"categories":[]},"Escape The Bathroom":{"path":"flash/?game=escape-the-bathroom","aliases":[],"categories":[]},"Escape The Freezer":{"path":"flash/?game=escape-the-freezer","aliases":[],"categories":[]},"Escape The Shack":{"path":"flash/?game=escape-the-shack","aliases":[],"categories":[]},"Family Feud":{"path":"family-feud","aliases":[],"categories":[]},"Fireboy and Watergirl in the Forest Temple":{"path":"fireboy-and-watergirl","aliases":["Fireboy and Watergirl","Fireboy and Watergirl 1"],"categories":[]},"Fireboy and Watergirl in the Light Temple":{"path":"fireboy-and-watergirl-2","aliases":["Fireboy and Watergirl 2"],"categories":[]},"Fireboy and Watergirl in the Ice Temple":{"path":"fireboy-and-watergirl-3","aliases":["Fireboy and Watergirl 3"],"categories":[]},"Fireboy and Watergirl in the Crystal Temple":{"path":"fireboy-and-watergirl-4","aliases":["Fireboy and Watergirl 4"],"categories":[]},"Five Nights at Epstein's":{"path":"five-nights-at-epsteins","aliases":[],"categories":[]},"Flash Chess":{"path":"flash/?game=flash-chess","aliases":[],"categories":[]},"Friday Night Funkin":{"path":"friday-night-funkin","aliases":[],"categories":[]},"Factory Balls":{"path":"flash/?game=factory-balls","aliases":[],"categories":[]},"Flappy Bird":{"path":"flappy-bird","aliases":[],"categories":[]},"Geometry Dash Lite":{"path":"geometry-dash-lite","aliases":[],"categories":[]},"Geometry Dash Remastered":{"path":"geometry-dash-remastered","aliases":[],"categories":[]},"Geometry Vibes":{"path":"geometry-vibes","aliases":[],"categories":[]},"Geometry Vibes Monster":{"path":"geometry-vibes-monster","aliases":[],"categories":[]},"Geometry Vibes X-Ball":{"path":"geometry-vibes-x-ball","aliases":[],"categories":[]},"Getaway Shootout":{"path":"getaway-shootout","aliases":[],"categories":[]},"Gons io":{"path":"gons-io","aliases":[],"categories":[]},"Gun Mayhem":{"path":"flash/?game=gun-mayhem","aliases":[],"categories":[]},"Gun Mayhem 2":{"path":"flash/?game=gun-mayhem-2","aliases":[],"categories":[]},"Gun Spin":{"path":"gunspin","aliases":[],"categories":[]},"Henry Stickmin 0: Breaking The Bank":{"path":"flash/?game=breaking-the-bank","aliases":[],"categories":[]},"Henry Stickmin 1: Escaping The Prison":{"path":"flash/?game=escaping-the-prison","aliases":[],"categories":[]},"Henry Stickmin 2: Stealing The Diamond":{"path":"flash/?game=stealing-the-diamond","aliases":[],"categories":[]},"Henry Stickmin 3: Infiltrating The Airship":{"path":"flash/?game=infiltrating-the-airship","aliases":[],"categories":[]},"Henry Stickmin 4: Fleeing the Complex":{"path":"flash/?game=fleeing-the-complex","aliases":[],"categories":[]},"Hextris":{"path":"hextris","aliases":[],"categories":[]},"Incremancer":{"path":"incremancer","aliases":[],"categories":[]},"Learn To Fly":{"path":"flash/?game=learn-to-fly","aliases":[],"categories":[]},"Learn To Fly 2":{"path":"flash/?game=learn-to-fly-2","aliases":[],"categories":[]},"Learn To Fly 3":{"path":"flash/?game=learn-to-fly-3","aliases":[],"categories":[]},"Learn To Fly Idle":{"path":"flash/?game=learn-to-fly-idle","aliases":[],"categories":[]},"Lion Solider's Vengeance":{"path":"lion-soldiers-vengeance","aliases":[],"cateogires":[]},"Idle Breakout":{"path":"idle-breakout","aliases":[],"categories":[]},"Maptroid":{"path":"maptroid","aliases":[],"categories":[]},"Mario":{"path":"mario-game","aliases":[],"categories":[]},"Monkey Mart":{"path":"monkey-mart","aliases":[],"categories":[]},"MotoX3M":{"path":"motox3m","aliases":[],"categories":[]},"N-gon":{"path":"n-gon","aliases":[],"categories":[]},"N Step Steve Part 1":{"path":"n-step-steve-part-1","aliases":[],"categories":[]},"N Step Steve Part 2":{"path":"n-step-steve-part-2","aliases":[],"categories":[]},"OvO":{"path":"ovo/1.4.4","aliases":[],"categories":[]},"OvO 2":{"path":"ovo/2.0.2alpha","aliases":[],"categories":[]},"Papas Pizzeria":{"path":"flash/?game=papas-pizzaria","aliases":[],"categories":[]},"Papas Freezeria":{"path":"flash/?game=papas-freezeria","aliases":[],"categories":[]},"Particle Clicker":{"path":"particle-clicker","aliases":[],"categories":[]},"Planet Life":{"path":"planet-life","aliases":[],"categories":[]},"P.craft":{"path":"pcraft","aliases":[],"categories":[]},"Progress Knight Quest":{"path":"progress-knight-quest","aliases":[],"categories":[]},"Progress Knight Reborn":{"path":"progress-knight-reborn","aliases":[],"categories":[]},"Pull Of War":{"path":"pull-of-war","aliases":[],"categories":[]},"Reach The Core":{"path":"reach-the-core","aliases":[],"categories":[]},"Restless Wing Syndrome":{"path":"restless-wing-syndrome","aliases":[],"categories":[]},"Raft Wars":{"path":"flash/?game=raft-wars","aliases":[],"categories":[]},"Raft Wars 2":{"path":"flash/?game=raft-wars-2","aliases":[],"categories":[]},"Retro Bowl":{"path":"retro-bowl","aliases":[],"categories":[]},"Retro Bowl Old":{"path":"retro-bowl-old","aliases":[],"categories":[]},"Rift Shift":{"path":"rift-shift","aliases":[],"categories":[]},"Rocket League 2D":{"path":"rocket-league-2d","aliases":[],"categories":[]},"Rogue Soul":{"path":"flash/?game=rogue-soul","aliases":[],"categories":[]},"Rogue Soul 2":{"path":"flash/?game=rogue-soul-2","aliases":[],"categories":[]},"Riddle School":{"path":"flash/?game=riddle-school","aliases":[],"categories":["escape"]},"Riddle School 2":{"path":"flash/?game=riddle-school-2","aliases":[],"categories":["escape"]},"Riddle School 3":{"path":"flash/?game=riddle-school-3","aliases":[],"categories":["escape"]},"Riddle School 4":{"path":"flash/?game=riddle-school-4","aliases":[],"categories":["escape"]},"Riddle School 5":{"path":"flash/?game=riddle-school-5","aliases":[],"categories":["escape"]},"Riddle Transfer":{"path":"flash/?game=riddle-transfer","aliases":[],"categories":["escape"]},"Riddle Transfer 2":{"path":"flash/?game=riddle-transfer-2","aliases":[],"categories":["escape"]},"Rooftop Snipers":{"path":"rooftop-snipers","aliases":[],"categories":[]},"Rookie Bowman":{"path":"rookie-bowman","aliases":[],"categories":[]},"Run 3":{"path":"run-3","aliases":[],"categories":[]},"Run 3 Beta (may not work)":{"path":"run-3-beta","aliases":[],"categories":[]},"Sabercut":{"path":"sabercut","aliases":[],"categories":[]},"Sandspiel":{"path":"sandspiel","aliases":[],"categories":[]},"Scuba Bear":{"path":"scuba-bear","aliases":[],"categories":[]},"Shadow Fight":{"path":"shadow-fight","aliases":[],"categories":[]},"Slice Master":{"path":"slice-master","aliases":[],"categories":[]},"Slope":{"path":"slope","aliases":[],"categories":[]},"Smash Karts":{"path":"smash-karts","aliases":[],"categories":[]},"Soccer Random":{"path":"soccer-random","aliases":[],"categories":[]},"Station Saturn":{"path":"station-saturn","aliases":[],"categories":[]},"Stickman Hook":{"path":"stickman-hook","aliases":[],"categories":[]},"Subway Surfers":{"path":"subway-surfers","aliases":[],"categories":[]},"Subway Surfers New York":{"path":"subway-surfers-ny","aliases":[],"categories":[]},"Stick RPG Complete":{"path":"flash/?game=stick-rpg-complete","aliases":[],"categories":[]},"Stick War":{"path":"flash/?game=stick-war","aliases":[],"categories":[]},"Submachine":{"path":"flash/?game=submachine","aliases":[],"categories":["escape"]},"Submachine 2":{"path":"flash/?game=submachine-2","aliases":[],"categories":["escape"]},"Submachine 3":{"path":"flash/?game=submachine-3","aliases":[],"categories":["escape"]},"Submachine 4":{"path":"flash/?game=submachine-4","aliases":[],"categories":["escape"]},"Submachine 5":{"path":"flash/?game=submachine-5","aliases":[],"categories":["escape"]},"Submachine 6":{"path":"flash/?game=submachine-6","aliases":[],"categories":["escape"]},"Submachine 7":{"path":"flash/?game=submachine-7","aliases":[],"categories":["escape"]},"Submachine 8":{"path":"flash/?game=submachine-8","aliases":[],"categories":["escape"]},"Submachine 9":{"path":"flash/?game=submachine-9","aliases":[],"categories":["escape"]},"Submachine 10":{"path":"flash/?game=submachine-10","aliases":[],"categories":["escape"]},"Submachine 0":{"path":"flash/?game=submachine-0","aliases":[],"categories":["escape"]},"Submachine 32 Chambers":{"path":"flash/?game=submachine-32-chambers","aliases":[],"categories":["escape"]},"Submachine FLF":{"path":"flash/?game=submachine-flf","aliases":[],"categories":["escape"]},"Sugar Sugar":{"path":"flash/?game=sugar-sugar","aliases":[],"categories":[]},"Swords and Souls":{"path":"flash/?game=swords-and-souls","aliases":[],"categories":[]},"The Final Earth":{"path":"the-final-earth","aliases":[],"categories":[]},"The Treasure":{"path":"the-treasure","aliases":[],"categories":["escape"]},"There Is No Game":{"path":"there-is-no-game","aliases":[],"categories":[]},"Tic Tac What?":{"path":"tic-tac-what","aliases":[],"categories":[]},"Time Shooter":{"path":"time-shooter","aliases":[],"categories":[]},"Time Shooter 3":{"path":"time-shooter-3","aliases":[],"categories":[]},"Tiny Fishing":{"path":"tiny-fishing","aliases":[],"categories":[]},"Trace":{"path":"trace","aliases":[],"categories":["escape"]},"Tanuki Sunset":{"path":"tanuki-sunset","aliases":[],"categories":[]},"Temple Run 2":{"path":"temple-run-2","aliases":[],"categories":[]},"The Impossible Quiz":{"path":"flash/?game=the-impossible-quiz","aliases":[],"categories":[]},"This Is The Only Level":{"path":"flash/?game=this-is-the-only-level","aliases":[],"categories":[]},"This Is The Only Level 2":{"path":"flash/?game=this-is-the-only-level-2","aliases":[],"categories":[]},"Tunnel Rush":{"path":"tunnel-rush","aliases":[],"categories":[]},"Two Ball 3d":{"path":"two-ball-3d","aliases":[],"categories":[]},"Vex 3":{"path":"vex-3","aliases":[],"categories":[]},"Vex 4":{"path":"vex-4","aliases":[],"categories":[]},"Vex 5":{"path":"vex-5","aliases":[],"categories":[]},"Vex 6":{"path":"vex-6","aliases":[],"categories":[]},"Vex 7":{"path":"vex-7","aliases":[],"categories":[]},"Volley Random":{"path":"volley-random","aliases":[],"categories":[]},"Web OSU":{"path":"web-osu","aliases":[],"categories":[]},"Ultimate Chess":{"path":"flash/?game=ultimate-chess","aliases":[],"categories":[]},"X Trench Run":{"path":"x-trench-run","aliases":[],"categories":[]},"Yohoho":{"path":"yohoho","aliases":[],"categories":[]}},"themes":{},"config":{"proxy":true,"proxyPath":"https://monkey.nordparrot.ro"}} \ No newline at end of file