added fnae
@@ -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": [],
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 389 KiB |
|
After Width: | Height: | Size: 539 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 545 KiB |
|
After Width: | Height: | Size: 538 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 519 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,246 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Five Nights At Epstein's - Web Version</title>
|
||||
<base href="/">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 预加载动画 -->
|
||||
<div id="preloader">
|
||||
<div class="preloader-content">
|
||||
<div class="preloader-logo">FIVE NIGHTS<br>AT EPSTEIN'S</div>
|
||||
<div class="preloader-spinner">
|
||||
<div class="spinner-ring"></div>
|
||||
<div class="spinner-ring"></div>
|
||||
<div class="spinner-ring"></div>
|
||||
</div>
|
||||
<div class="preloader-text">LOADING<span class="loading-dots">...</span></div>
|
||||
<div class="preloader-progress">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<div class="preloader-percentage" id="preloader-percentage">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="game-container">
|
||||
<!-- 主游戏画面 -->
|
||||
<div id="game-screen">
|
||||
<img id="current-scene" src="" alt="" style="display: none;">
|
||||
|
||||
<!-- 交互热区 -->
|
||||
<div id="hotspots"></div>
|
||||
|
||||
<!-- UI 覆盖层 -->
|
||||
<div id="ui-overlay">
|
||||
<!-- 左上角:时间和夜数 -->
|
||||
<div id="top-left-ui">
|
||||
<div id="time-display"><span id="time-value">12 AM</span></div>
|
||||
<div id="night-display">NIGHT <span id="night-value">1</span></div>
|
||||
</div>
|
||||
|
||||
<!-- 右下角:氧气和通风管图标 -->
|
||||
<div id="bottom-right-ui">
|
||||
<div id="oxygen-display">
|
||||
<img src="/assets/images/fa3.png" alt="Vent" class="vent-icon">
|
||||
<span id="power-value">100</span><span class="percent-sign">%</span><span
|
||||
class="oxygen-unit">O<sub>2</sub></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 摄像头面板 -->
|
||||
<div id="camera-panel" class="hidden">
|
||||
<div id="current-cam-label">CAM 11</div>
|
||||
<div id="camera-error-label">ERR</div>
|
||||
<!-- 角色图层容器 -->
|
||||
<div id="character-overlay"></div>
|
||||
<div id="camera-grid"></div>
|
||||
<button id="shock-hawking-btn">ELECTROCUTE</button>
|
||||
<button id="play-sound-btn">PLAY SOUND</button>
|
||||
<!-- 摄像头故障雪花视频 -->
|
||||
<video id="camera-static-video" loop muted playsinline>
|
||||
<source src="/assets/vedio/202512191935.webm" type="video/webm">
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- 过场动画 -->
|
||||
<div id="cutscene" class="hidden">
|
||||
<img src="/assets/images/cutscene.png" alt="Cutscene">
|
||||
<div class="cutscene-hint">Click to continue...</div>
|
||||
</div>
|
||||
|
||||
<!-- 每晚开始场景 -->
|
||||
<div id="night-intro" class="hidden">
|
||||
<div id="night-intro-text">NIGHT 1</div>
|
||||
</div>
|
||||
|
||||
<!-- 主菜单 -->
|
||||
<div id="main-menu">
|
||||
<img id="star-icon" class="hidden" src="/assets/images/star.png" alt="Star">
|
||||
<img id="star-icon-2" class="hidden" src="/assets/images/star.png" alt="Star 2">
|
||||
<img id="star-icon-3" class="hidden" src="/assets/images/star.png" alt="Star 3">
|
||||
|
||||
<h1>FIVE<br>NIGHTS<br>AT<br>EPSTEIN'S</h1>
|
||||
<button id="start-game">NEW GAME</button>
|
||||
<button id="continue-game" class="hidden">CONTINUE</button>
|
||||
<button id="special-night-btn" class="hidden">EPSTEIN'S SPECIAL NIGHT</button>
|
||||
<button id="custom-night-btn" class="hidden">CUSTOM NIGHT</button>
|
||||
<div class="copyright">© EVAN PRODUCTIONS | HTML5 remake by <a
|
||||
href="https://fivenightsatepsteins.org/fivenightsatepsteins-online" target="_blank"
|
||||
style="color: inherit; text-decoration: underline;">killlala1213</a></div>
|
||||
<div class="version">v1.2.3</div>
|
||||
</div>
|
||||
|
||||
<!-- 音量控制按钮 - 独立于主菜单 -->
|
||||
<button id="volume-btn" title="Volume Settings">SOUND SETTINGS</button>
|
||||
|
||||
<!-- 音量设置面板 -->
|
||||
<div id="volume-panel" class="hidden">
|
||||
<h3>VOLUME SETTINGS</h3>
|
||||
|
||||
<div class="volume-item">
|
||||
<label>Master Volume</label>
|
||||
<div class="volume-slider-container">
|
||||
<input type="range" id="master-volume" min="0" max="100" value="70" />
|
||||
<span class="volume-percent">70%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volume-item">
|
||||
<label>Game Background Music</label>
|
||||
<div class="volume-slider-container">
|
||||
<input type="range" id="game-bg-volume" min="0" max="100" value="70" />
|
||||
<span class="volume-percent">70%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volume-item">
|
||||
<label>Menu Music</label>
|
||||
<div class="volume-slider-container">
|
||||
<input type="range" id="menu-music-volume" min="0" max="100" value="70" />
|
||||
<span class="volume-percent">70%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volume-item">
|
||||
<label>Jumpscare Sounds</label>
|
||||
<div class="volume-slider-container">
|
||||
<input type="range" id="jumpscare-volume" min="0" max="100" value="70" />
|
||||
<span class="volume-percent">70%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volume-item">
|
||||
<label>Trump Vent Crawling</label>
|
||||
<div class="volume-slider-container">
|
||||
<input type="range" id="vent-crawling-volume" min="0" max="100" value="70" />
|
||||
<span class="volume-percent">70%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="close-volume-panel">CLOSE</button>
|
||||
</div>
|
||||
|
||||
<!-- Custom Night 选择界面 -->
|
||||
<div id="custom-night-menu" class="hidden">
|
||||
<h1>CUSTOM NIGHT</h1>
|
||||
<div class="custom-night-controls">
|
||||
<!-- Epstein AI -->
|
||||
<div class="ai-control">
|
||||
<div class="ai-name">EPSTEIN</div>
|
||||
<div class="ai-slider-container">
|
||||
<button class="ai-btn-minus" data-ai="epstein">-</button>
|
||||
<input type="range" id="epstein-slider" class="ai-slider" min="0" max="20" value="0">
|
||||
<button class="ai-btn-plus" data-ai="epstein">+</button>
|
||||
</div>
|
||||
<div class="ai-value" id="epstein-value">0</div>
|
||||
</div>
|
||||
|
||||
<!-- Trump AI -->
|
||||
<div class="ai-control">
|
||||
<div class="ai-name">TRUMP</div>
|
||||
<div class="ai-slider-container">
|
||||
<button class="ai-btn-minus" data-ai="trump">-</button>
|
||||
<input type="range" id="trump-slider" class="ai-slider" min="0" max="20" value="0">
|
||||
<button class="ai-btn-plus" data-ai="trump">+</button>
|
||||
</div>
|
||||
<div class="ai-value" id="trump-value">0</div>
|
||||
</div>
|
||||
|
||||
<!-- Hawking AI -->
|
||||
<div class="ai-control">
|
||||
<div class="ai-name">HAWKING</div>
|
||||
<div class="ai-slider-container">
|
||||
<button class="ai-btn-minus" data-ai="hawking">-</button>
|
||||
<input type="range" id="hawking-slider" class="ai-slider" min="0" max="20" value="0">
|
||||
<button class="ai-btn-plus" data-ai="hawking">+</button>
|
||||
</div>
|
||||
<div class="ai-value" id="hawking-value">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="custom-night-buttons">
|
||||
<button id="start-custom-night">START</button>
|
||||
<button id="back-to-menu">BACK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 静态噪点层 -->
|
||||
<canvas id="static-canvas"></canvas>
|
||||
|
||||
<!-- 背景音乐 -->
|
||||
<audio id="menu-music" loop>
|
||||
<source src="/assets/sounds/music3.ogg" type="audio/ogg">
|
||||
</audio>
|
||||
|
||||
<!-- 游戏结束画面 -->
|
||||
<div id="game-over" class="hidden">
|
||||
<!-- 雪花视频背景 -->
|
||||
<video id="game-over-static" loop muted playsinline autoplay>
|
||||
<source src="/assets/vedio/202512191935.webm" type="video/webm">
|
||||
</video>
|
||||
<div id="game-over-content">
|
||||
<h2 id="game-over-text"></h2>
|
||||
<p id="game-over-subtitle" class="hidden"></p>
|
||||
<button id="restart">Restart</button>
|
||||
<button id="main-menu-btn">Main Menu</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏教程提示 -->
|
||||
<div id="tutorial-overlay" class="hidden">
|
||||
<div id="tutorial-content">
|
||||
<h2>DEFEND YOURSELF AGAINST EPSTEIN</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<button id="tutorial-got-it">GOT IT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/GameState.js"></script>
|
||||
<script src="js/AssetManager.js"></script>
|
||||
<script src="js/UIManager.js"></script>
|
||||
<script src="js/CameraSystem.js"></script>
|
||||
<script src="js/EnemyAI.js"></script>
|
||||
<script src="js/InputHandler.js"></script>
|
||||
<script src="js/StaticNoise.js"></script>
|
||||
<script src="js/ScaryFaceFlicker.js"></script>
|
||||
<script src="js/Game.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
<!-- <script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"469f83475e2048ccb6d048799adda2a6","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script> -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 之前设置
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}')`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 ?
|
||||
'<span class="option-arrow" style="color: #0f0; margin-right: 1.5vw; width: 2vw;">></span><span>Open Air Vents</span><span id="vents-dots" style="margin-left: 1vw; direction: ltr; font-family: \'Courier New\', monospace;"></span>' :
|
||||
'<span class="option-arrow" style="color: #0f0; margin-right: 1.5vw; width: 2vw;">></span><span>Close Air Vents</span><span id="vents-dots" style="margin-left: 1vw; direction: ltr; font-family: \'Courier New\', monospace;"></span>';
|
||||
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 = '<span class="option-arrow" style="color: transparent; margin-right: 1.5vw; width: 2vw;">></span><span>Restart Cameras</span><span id="camera-dots" style="margin-left: 1vw; direction: ltr; font-family: \'Courier New\', monospace;"></span><span id="camera-status" style="margin-left: auto; padding-right: 2vw; direction: ltr;"></span>';
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// 如果失败,等待用户在游戏内点击
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
After Width: | Height: | Size: 1.7 MiB |