added fnae

This commit is contained in:
MetaGG2
2026-02-25 22:24:59 -05:00
parent 018d79a72b
commit 32c445e21e
63 changed files with 8215 additions and 1 deletions
+5
View File
@@ -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": [],
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+246
View File
@@ -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);
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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;">&gt;</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;">&gt;</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;">&gt;</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);
}
}
+325
View File
@@ -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);
// 如果失败,等待用户在游戏内点击
});
}
}
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long