From a91a61a28813cb59193e9459163d04982c6f864d Mon Sep 17 00:00:00 2001 From: kuhyx Date: Fri, 1 Aug 2025 16:40:12 +0200 Subject: [PATCH] Initial commit: Danger Field Game with directional audio - Open field gameplay with boundary walls only - Three types of squares: safe (white), visual danger (red), audio danger (hidden) - Directional audio system with stereo panning and pitch variation - Proximity warnings with spatial audio cues - Multi-sensory feedback (visual, audio, haptic) - HTML5 Canvas rendering with smooth animations - Web Audio API integration for rich sound effects - Responsive design and modern JavaScript ES6+ modules --- .github/copilot-instructions.md | 26 + .gitignore | 24 + README.md | 126 ++++ index.html | 28 + package-lock.json | 1026 +++++++++++++++++++++++++++++++ package.json | 14 + public/vite.svg | 1 + src/audio.js | 209 +++++++ src/game.js | 177 ++++++ src/input.js | 79 +++ src/main.js | 11 + src/maze.js | 224 +++++++ src/player.js | 71 +++ src/style.css | 156 +++++ 14 files changed, 2172 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/vite.svg create mode 100644 src/audio.js create mode 100644 src/game.js create mode 100644 src/input.js create mode 100644 src/main.js create mode 100644 src/maze.js create mode 100644 src/player.js create mode 100644 src/style.css diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..d139253 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,26 @@ +# Copilot Instructions + + + +## Project Overview +This is a labyrinth game built with vanilla JavaScript and HTML5 Canvas. The game features: + +- Player navigation through a maze using keyboard controls +- Visual feedback for dangerous vs safe squares (different colors/patterns) +- Audio feedback using Web Audio API for dangerous areas +- Haptic vibration feedback on supported devices +- Canvas-based rendering for smooth graphics + +## Code Style Guidelines +- Use modern ES6+ JavaScript features +- Implement modular code structure with separate files for game logic, rendering, and audio +- Use clear variable and function names that describe their purpose +- Add comments for complex game mechanics and algorithms + +## Key Components +- Game engine with update/render loop +- Player movement and collision detection +- Maze generation or predefined layouts +- Audio system for sound effects +- Vibration API integration for haptic feedback +- Visual effects and animations for enhanced user experience diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md new file mode 100644 index 0000000..0811439 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +# Danger Field Game + +A browser-based danger field game where players navigate through an open area filled with hidden and visible dangers. The game provides multiple types of feedback to enhance the gaming experience. + +## Features + +### Core Gameplay +- **Player Movement**: Navigate using WASD keys or arrow keys +- **Open Field**: No internal walls - only outer boundary is impassable +- **Mixed Dangers**: Scattered visual and audio danger squares throughout the field +- **Goal**: Reach the exit (🏁) while avoiding various types of dangers + +### Feedback Systems +- **Visual Feedback**: + - **Safe squares**: Pure white appearance + - **Visual danger squares**: Animated red with warning patterns (no audio) + - **Audio danger squares**: Look identical to safe squares (white) + - Exit square pulses green with a flag symbol + - Player has a glowing green circle with subtle animations + +- **Audio Feedback**: + - **Directional proximity warnings**: Spatial audio indicates direction of hidden dangers + - **Left/Right**: Sound pans to the side where danger is located + - **Up/Down**: Pitch changes (higher for above, lower for below) + - **Multiple directions**: Combined audio cues for complex danger patterns + - **Danger sounds**: When stepping on audio danger squares + - **Victory melody**: When completing the maze + - Built with Web Audio API with stereo panning and pitch modulation + +- **Haptic Feedback**: + - **Visual danger**: Quick double vibration (no audio) + - **Audio danger**: Long vibration pattern with audio + - **Victory**: Celebration vibration pattern + - Toggle on/off functionality + +### Technical Features +- **HTML5 Canvas Rendering**: Smooth 60fps graphics +- **Modular Architecture**: Clean separation of concerns +- **Responsive Design**: Works on desktop and mobile devices +- **Modern JavaScript**: ES6+ features and modules +- **Open Field Design**: No maze walls, only boundary constraints + +## Getting Started + +### Prerequisites +- Node.js (v20+ recommended) +- Modern web browser with Web Audio API support + +### Installation +1. Clone or download the project +2. Install dependencies: + ```bash + npm install + ``` + +3. Start the development server: + ```bash + npm run dev + ``` + +4. Open your browser and navigate to the provided local URL (typically `http://localhost:5173`) + +### Building for Production +```bash +npm run build +``` + +## How to Play + +1. **Movement**: Use WASD keys or arrow keys to move your character (green circle) +2. **Visual Dangers**: Red animated squares are dangerous - avoid them for safety +3. **Hidden Audio Dangers**: Some white squares look safe but are dangerous +4. **Directional Audio Cues**: + - **Left/Right panning**: Sound comes from the side where hidden dangers are located + - **Pitch variation**: Higher pitch for dangers above, lower pitch for dangers below + - **Rhythmic patterns**: Different timing patterns indicate direction +5. **Open Navigation**: Move freely through the field - only the outer boundary blocks you +6. **Find the Exit**: Navigate to the green flag (🏁) to complete the level +7. **Headphone Recommended**: Use headphones or good speakers for optimal directional audio experience + +## Game Controls + +- **W / ↑**: Move up +- **A / ←**: Move left +- **S / ↓**: Move down +- **D / →**: Move right +- **Sound Toggle**: Enable/disable audio feedback +- **Vibration Toggle**: Enable/disable haptic feedback + +## Technical Architecture + +The game is built with a modular architecture: + +- `game.js`: Main game loop and state management +- `player.js`: Player entity and rendering +- `maze.js`: Field generation and rendering +- `audio.js`: Sound effects and Web Audio API integration +- `input.js`: Keyboard input handling +- `style.css`: Game styling and responsive design + +## Browser Compatibility + +- **Audio**: Requires Web Audio API support with stereo panning (all modern browsers) +- **Vibration**: Requires Vibration API support (mobile browsers, some desktop) +- **Graphics**: HTML5 Canvas support (all modern browsers) +- **Directional Audio**: Best experienced with headphones or stereo speakers + +## Development + +This project uses Vite for fast development and building. The codebase follows modern JavaScript standards with ES6+ modules. + +### Project Structure +``` +src/ +├── main.js # Entry point +├── game.js # Main game class +├── player.js # Player entity +├── maze.js # Maze generation and rendering +├── audio.js # Audio system +├── input.js # Input handling +└── style.css # Styling +``` + +## License + +This project is open source and available under the MIT License. diff --git a/index.html b/index.html new file mode 100644 index 0000000..3611287 --- /dev/null +++ b/index.html @@ -0,0 +1,28 @@ + + + + + + + Danger Field Game + + +
+
+

Danger Field Game

+
+

Use WASD or Arrow Keys to move

+

⚠️ Red squares are visually dangerous!

+

🔊 Listen carefully - directional audio warns of hidden dangers!

+

🎧 Use headphones for best directional audio experience

+
+ +
+ + +
+
+
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..15afbb0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1026 @@ +{ + "name": "gamejam2025", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gamejam2025", + "version": "0.0.0", + "devDependencies": { + "vite": "^7.0.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3f56886 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "gamejam2025", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^7.0.4" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/audio.js b/src/audio.js new file mode 100644 index 0000000..4cb055c --- /dev/null +++ b/src/audio.js @@ -0,0 +1,209 @@ +export class AudioSystem { + constructor() { + this.audioContext = null; + this.gainNode = null; + this.activeSounds = new Set(); + this.initialized = false; + + this.initializeAudio(); + } + + async initializeAudio() { + try { + // Create audio context (requires user interaction) + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + this.gainNode = this.audioContext.createGain(); + this.gainNode.connect(this.audioContext.destination); + this.gainNode.gain.value = 0.3; // Set volume to 30% + + this.initialized = true; + } catch (error) { + console.warn('Web Audio API not supported:', error); + } + } + + async ensureAudioContext() { + if (!this.initialized) { + await this.initializeAudio(); + } + + if (this.audioContext && this.audioContext.state === 'suspended') { + await this.audioContext.resume(); + } + } + + createOscillator(frequency, type = 'sine', duration = 0.2, panValue = 0, pitchBend = 0) { + if (!this.audioContext || !this.initialized) return null; + + const oscillator = this.audioContext.createOscillator(); + const envelope = this.audioContext.createGain(); + const panner = this.audioContext.createStereoPanner(); + + oscillator.connect(envelope); + envelope.connect(panner); + panner.connect(this.gainNode); + + oscillator.frequency.value = frequency + pitchBend; + oscillator.type = type; + + // Set stereo panning (-1 = left, 0 = center, 1 = right) + panner.pan.value = Math.max(-1, Math.min(1, panValue)); + + // Create ADSR envelope + const now = this.audioContext.currentTime; + envelope.gain.setValueAtTime(0, now); + envelope.gain.linearRampToValueAtTime(1, now + 0.01); // Attack + envelope.gain.exponentialRampToValueAtTime(0.01, now + duration); // Decay + + oscillator.start(now); + oscillator.stop(now + duration); + + this.activeSounds.add(oscillator); + oscillator.onended = () => { + this.activeSounds.delete(oscillator); + }; + + return oscillator; + } + + createNoise(duration = 0.1) { + if (!this.audioContext || !this.initialized) return null; + + const bufferSize = this.audioContext.sampleRate * duration; + const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate); + const output = buffer.getChannelData(0); + + // Generate white noise + for (let i = 0; i < bufferSize; i++) { + output[i] = Math.random() * 2 - 1; + } + + const source = this.audioContext.createBufferSource(); + const filter = this.audioContext.createBiquadFilter(); + const envelope = this.audioContext.createGain(); + + source.buffer = buffer; + source.connect(filter); + filter.connect(envelope); + envelope.connect(this.gainNode); + + filter.type = 'lowpass'; + filter.frequency.value = 300; + + const now = this.audioContext.currentTime; + envelope.gain.setValueAtTime(0.5, now); + envelope.gain.linearRampToValueAtTime(0, now + duration); + + source.start(now); + + this.activeSounds.add(source); + source.onended = () => { + this.activeSounds.delete(source); + }; + + return source; + } + + async playDangerSound() { + await this.ensureAudioContext(); + + // Create a danger sound with multiple frequencies and noise + this.createOscillator(150, 'sawtooth', 0.3); + this.createOscillator(200, 'square', 0.2); + this.createNoise(0.15); + + // Add a low rumble + setTimeout(() => { + this.createOscillator(80, 'sine', 0.4); + }, 100); + } + + async playProximitySound() { + await this.ensureAudioContext(); + + // Create a subtle warning sound for proximity to audio danger + this.createOscillator(300, 'sine', 0.1); + this.createOscillator(250, 'sine', 0.1); + } + + async playDirectionalProximitySound(directions) { + await this.ensureAudioContext(); + + // Calculate the dominant direction for panning + let panValue = 0; + let pitchModifier = 0; + + // Horizontal panning (left/right) + if (directions.left && !directions.right) { + panValue = -0.7; // Pan left + } else if (directions.right && !directions.left) { + panValue = 0.7; // Pan right + } + + // Vertical pitch variation (up/down) + if (directions.up && !directions.down) { + pitchModifier = 50; // Higher pitch for above + } else if (directions.down && !directions.up) { + pitchModifier = -50; // Lower pitch for below + } + + // Create directional warning sounds + this.createOscillator(300, 'sine', 0.15, panValue, pitchModifier); + this.createOscillator(250, 'sine', 0.15, panValue, pitchModifier * 0.5); + + // Add subtle rhythmic variation based on direction + if (directions.up) { + // Quick double beep for above + setTimeout(() => { + this.createOscillator(350, 'sine', 0.05, panValue, pitchModifier); + }, 80); + } else if (directions.down) { + // Slower, deeper sound for below + this.createOscillator(200, 'sine', 0.2, panValue, pitchModifier); + } + } + + async playWinSound() { + await this.ensureAudioContext(); + + // Create a victory melody + const notes = [262, 330, 392, 523]; // C, E, G, C (one octave higher) + + notes.forEach((frequency, index) => { + setTimeout(() => { + this.createOscillator(frequency, 'sine', 0.4); + }, index * 100); + }); + + // Add harmony + setTimeout(() => { + this.createOscillator(659, 'sine', 0.6); // E + }, 200); + } + + playMoveSound() { + if (!this.initialized) return; + + // Subtle movement sound + this.createOscillator(440, 'sine', 0.05); + } + + stopAll() { + this.activeSounds.forEach(sound => { + try { + if (sound.stop) { + sound.stop(); + } + } catch (error) { + // Sound may have already stopped + } + }); + this.activeSounds.clear(); + } + + setVolume(volume) { + if (this.gainNode) { + this.gainNode.gain.value = Math.max(0, Math.min(1, volume)); + } + } +} diff --git a/src/game.js b/src/game.js new file mode 100644 index 0000000..639d65c --- /dev/null +++ b/src/game.js @@ -0,0 +1,177 @@ +import { Player } from './player.js'; +import { Maze } from './maze.js'; +import { AudioSystem } from './audio.js'; +import { InputHandler } from './input.js'; + +export class Game { + constructor(canvas) { + this.canvas = canvas; + this.ctx = canvas.getContext('2d'); + this.running = false; + this.lastTime = 0; + + // Game settings + this.cellSize = 40; + this.cols = Math.floor(canvas.width / this.cellSize); + this.rows = Math.floor(canvas.height / this.cellSize); + + // Initialize game systems + this.maze = new Maze(this.cols, this.rows); + this.player = new Player(1, 1, this.cellSize); + this.audioSystem = new AudioSystem(); + this.inputHandler = new InputHandler(); + + // Game state + this.soundEnabled = true; + this.vibrationEnabled = true; + this.lastProximityWarning = 0; + + this.setupControls(); + this.bindEvents(); + } + + setupControls() { + const soundToggle = document.getElementById('soundToggle'); + const vibrationToggle = document.getElementById('vibrationToggle'); + + soundToggle.addEventListener('click', () => { + this.soundEnabled = !this.soundEnabled; + soundToggle.textContent = `🔊 Sound: ${this.soundEnabled ? 'ON' : 'OFF'}`; + if (!this.soundEnabled) { + this.audioSystem.stopAll(); + } + }); + + vibrationToggle.addEventListener('click', () => { + this.vibrationEnabled = !this.vibrationEnabled; + vibrationToggle.textContent = `📳 Vibration: ${this.vibrationEnabled ? 'ON' : 'OFF'}`; + }); + } + + bindEvents() { + this.inputHandler.onMove = (direction) => { + this.movePlayer(direction); + }; + } + + movePlayer(direction) { + const newX = this.player.x + direction.x; + const newY = this.player.y + direction.y; + + // Check boundaries and walls + if (this.maze.canMoveTo(newX, newY)) { + this.player.moveTo(newX, newY); + + // Check if player stepped on a visual danger square + if (this.maze.isDangerousVisual(newX, newY)) { + this.handleVisualDanger(); + } + + // Check if player stepped on an audio danger square + if (this.maze.isDangerousAudio(newX, newY)) { + this.handleAudioDanger(); + } + + // Check if player reached the exit + if (this.maze.isExit(newX, newY)) { + this.handleWin(); + } + } + } + + handleVisualDanger() { + // Visual danger squares only provide vibration feedback (no audio) + if (this.vibrationEnabled && navigator.vibrate) { + navigator.vibrate([150, 50, 150]); // Quick double vibration + } + } + + handleAudioDanger() { + // Audio danger squares provide both sound and vibration + if (this.soundEnabled) { + this.audioSystem.playDangerSound(); + } + + if (this.vibrationEnabled && navigator.vibrate) { + navigator.vibrate([200, 100, 200]); // Pattern: vibrate-pause-vibrate + } + } + + handleWin() { + if (this.soundEnabled) { + this.audioSystem.playWinSound(); + } + + if (this.vibrationEnabled && navigator.vibrate) { + navigator.vibrate([100, 50, 100, 50, 100]); // Victory pattern + } + + setTimeout(() => { + alert('Congratulations! You completed the labyrinth!'); + this.resetGame(); + }, 500); + } + + resetGame() { + this.maze.generate(); + this.player.moveTo(1, 1); + } + + update(deltaTime) { + this.player.update(deltaTime); + + // Check proximity to audio danger squares and play directional warning sounds + if (this.soundEnabled) { + const directions = this.maze.getAudioDangerDirections(this.player.x, this.player.y); + if (directions.squares.length > 0) { + this.playDirectionalProximityWarning(directions); + } + } + } + + playDirectionalProximityWarning(directions) { + // Play a directional warning sound when near audio danger squares + // Use a timer to avoid playing too frequently + const now = Date.now(); + if (!this.lastProximityWarning || now - this.lastProximityWarning > 1000) { + this.audioSystem.playDirectionalProximitySound(directions); + this.lastProximityWarning = now; + } + } + + render() { + // Clear canvas + this.ctx.fillStyle = '#1a1a1a'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Render maze + this.maze.render(this.ctx, this.cellSize); + + // Render player + this.player.render(this.ctx); + } + + gameLoop(currentTime) { + if (!this.running) return; + + const deltaTime = currentTime - this.lastTime; + this.lastTime = currentTime; + + this.update(deltaTime); + this.render(); + + requestAnimationFrame((time) => this.gameLoop(time)); + } + + start() { + this.running = true; + this.maze.generate(); + this.lastTime = performance.now(); + requestAnimationFrame((time) => this.gameLoop(time)); + } + + stop() { + this.running = false; + this.audioSystem.stopAll(); + } +} diff --git a/src/input.js b/src/input.js new file mode 100644 index 0000000..9351b02 --- /dev/null +++ b/src/input.js @@ -0,0 +1,79 @@ +export class InputHandler { + constructor() { + this.keys = new Set(); + this.onMove = null; // Callback for movement + this.moveDelay = 150; // Milliseconds between moves + this.lastMoveTime = 0; + + this.bindEvents(); + } + + bindEvents() { + document.addEventListener('keydown', (e) => this.handleKeyDown(e)); + document.addEventListener('keyup', (e) => this.handleKeyUp(e)); + + // Prevent default behavior for game keys + document.addEventListener('keydown', (e) => { + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + }); + } + + handleKeyDown(e) { + this.keys.add(e.code); + this.processMovement(); + } + + handleKeyUp(e) { + this.keys.delete(e.code); + } + + processMovement() { + const now = Date.now(); + if (now - this.lastMoveTime < this.moveDelay) { + return; // Too soon to move again + } + + let direction = null; + + // Check for movement keys (WASD or Arrow keys) + if (this.keys.has('KeyW') || this.keys.has('ArrowUp')) { + direction = { x: 0, y: -1 }; + } else if (this.keys.has('KeyS') || this.keys.has('ArrowDown')) { + direction = { x: 0, y: 1 }; + } else if (this.keys.has('KeyA') || this.keys.has('ArrowLeft')) { + direction = { x: -1, y: 0 }; + } else if (this.keys.has('KeyD') || this.keys.has('ArrowRight')) { + direction = { x: 1, y: 0 }; + } + + if (direction && this.onMove) { + this.onMove(direction); + this.lastMoveTime = now; + } + } + + // Method to handle continuous key holding + update() { + if (this.keys.size > 0) { + this.processMovement(); + } + } + + // Get current input state + getInputState() { + return { + up: this.keys.has('KeyW') || this.keys.has('ArrowUp'), + down: this.keys.has('KeyS') || this.keys.has('ArrowDown'), + left: this.keys.has('KeyA') || this.keys.has('ArrowLeft'), + right: this.keys.has('KeyD') || this.keys.has('ArrowRight'), + }; + } + + // Clean up event listeners + destroy() { + document.removeEventListener('keydown', this.handleKeyDown); + document.removeEventListener('keyup', this.handleKeyUp); + } +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..ceb3988 --- /dev/null +++ b/src/main.js @@ -0,0 +1,11 @@ +import './style.css' +import { Game } from './game.js' + +// Initialize the game when the page loads +document.addEventListener('DOMContentLoaded', () => { + const canvas = document.getElementById('gameCanvas'); + const game = new Game(canvas); + + // Start the game + game.start(); +}); diff --git a/src/maze.js b/src/maze.js new file mode 100644 index 0000000..d830533 --- /dev/null +++ b/src/maze.js @@ -0,0 +1,224 @@ +export class Maze { + constructor(cols, rows) { + this.cols = cols; + this.rows = rows; + this.grid = []; + this.dangerousSquares = new Set(); + this.exitX = cols - 2; + this.exitY = rows - 2; + + // Cell types + this.WALL = 0; + this.SAFE = 1; + this.DANGEROUS_VISUAL = 2; // Visually dangerous, no audio + this.DANGEROUS_AUDIO = 3; // Looks safe, has audio when near + this.EXIT = 4; + } + + generate() { + // Initialize grid with safe squares + this.grid = Array(this.rows).fill().map(() => Array(this.cols).fill(this.SAFE)); + this.dangerousSquares.clear(); + + // Create outer boundary walls only + this.createOuterWalls(); + + // Add dangerous squares (about 30% of inner safe squares) + this.addDangerousSquares(); + + // Set exit (ensure it's not on the outer wall) + this.exitX = this.cols - 2; + this.exitY = this.rows - 2; + this.grid[this.exitY][this.exitX] = this.EXIT; + this.dangerousSquares.delete(`${this.exitX},${this.exitY}`); + } + + createOuterWalls() { + // Create walls only on the outer perimeter + for (let y = 0; y < this.rows; y++) { + for (let x = 0; x < this.cols; x++) { + if (x === 0 || x === this.cols - 1 || y === 0 || y === this.rows - 1) { + this.grid[y][x] = this.WALL; + } + } + } + } + + addDangerousSquares() { + const innerSafeSquares = []; + + // Find all inner safe squares (excluding outer wall boundary and player start) + for (let y = 1; y < this.rows - 1; y++) { + for (let x = 1; x < this.cols - 1; x++) { + if (this.grid[y][x] === this.SAFE && !(x === 1 && y === 1) && !(x === this.exitX && y === this.exitY)) { + innerSafeSquares.push({ x, y }); + } + } + } + + // Make 30% of inner safe squares dangerous (15% visual, 15% audio) + const totalDangerousCount = Math.floor(innerSafeSquares.length * 0.3); + const visualDangerousCount = Math.floor(totalDangerousCount * 0.5); + const audioDangerousCount = totalDangerousCount - visualDangerousCount; + + this.shuffle(innerSafeSquares); + + // Add visual-only dangerous squares + for (let i = 0; i < visualDangerousCount; i++) { + const { x, y } = innerSafeSquares[i]; + this.grid[y][x] = this.DANGEROUS_VISUAL; + this.dangerousSquares.add(`${x},${y}`); + } + + // Add audio-only dangerous squares + for (let i = visualDangerousCount; i < visualDangerousCount + audioDangerousCount; i++) { + const { x, y } = innerSafeSquares[i]; + this.grid[y][x] = this.DANGEROUS_AUDIO; + this.dangerousSquares.add(`${x},${y}`); + } + } + + shuffle(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + } + + canMoveTo(x, y) { + if (x < 0 || x >= this.cols || y < 0 || y >= this.rows) { + return false; + } + return this.grid[y][x] !== this.WALL; + } + + isDangerous(x, y) { + return this.grid[y][x] === this.DANGEROUS_VISUAL || this.grid[y][x] === this.DANGEROUS_AUDIO; + } + + isDangerousVisual(x, y) { + return this.grid[y][x] === this.DANGEROUS_VISUAL; + } + + isDangerousAudio(x, y) { + return this.grid[y][x] === this.DANGEROUS_AUDIO; + } + + isNearAudioDanger(playerX, playerY, range = 1) { + // Check if player is within range of any audio danger squares + for (let dy = -range; dy <= range; dy++) { + for (let dx = -range; dx <= range; dx++) { + const checkX = playerX + dx; + const checkY = playerY + dy; + if (checkX >= 0 && checkX < this.cols && checkY >= 0 && checkY < this.rows) { + if (this.grid[checkY][checkX] === this.DANGEROUS_AUDIO) { + return true; + } + } + } + } + return false; + } + + getAudioDangerDirections(playerX, playerY, range = 1) { + // Get all directions where audio danger squares are located + const directions = { + left: false, + right: false, + up: false, + down: false, + squares: [] + }; + + for (let dy = -range; dy <= range; dy++) { + for (let dx = -range; dx <= range; dx++) { + if (dx === 0 && dy === 0) continue; // Skip player's current position + + const checkX = playerX + dx; + const checkY = playerY + dy; + + if (checkX >= 0 && checkX < this.cols && checkY >= 0 && checkY < this.rows) { + if (this.grid[checkY][checkX] === this.DANGEROUS_AUDIO) { + directions.squares.push({ x: checkX, y: checkY, dx, dy }); + + // Determine primary directions + if (dx < 0) directions.left = true; + if (dx > 0) directions.right = true; + if (dy < 0) directions.up = true; + if (dy > 0) directions.down = true; + } + } + } + } + + return directions; + } + + isExit(x, y) { + return this.grid[y][x] === this.EXIT; + } + + render(ctx, cellSize) { + for (let y = 0; y < this.rows; y++) { + for (let x = 0; x < this.cols; x++) { + const pixelX = x * cellSize; + const pixelY = y * cellSize; + + switch (this.grid[y][x]) { + case this.WALL: + ctx.fillStyle = '#333333'; + ctx.fillRect(pixelX, pixelY, cellSize, cellSize); + // Add some texture to walls + ctx.fillStyle = '#444444'; + ctx.fillRect(pixelX + 2, pixelY + 2, cellSize - 4, cellSize - 4); + break; + + case this.SAFE: + case this.DANGEROUS_AUDIO: + // Safe squares and audio-danger squares look identical (white) + ctx.fillStyle = '#ffffff'; + ctx.fillRect(pixelX, pixelY, cellSize, cellSize); + // Add subtle grid lines + ctx.strokeStyle = '#e0e0e0'; + ctx.lineWidth = 1; + ctx.strokeRect(pixelX, pixelY, cellSize, cellSize); + break; + + case this.DANGEROUS_VISUAL: + // Visual-only danger squares - animated red appearance + const time = Date.now() * 0.003; + const intensity = (Math.sin(time + x + y) + 1) * 0.5; + const red = Math.floor(150 + intensity * 105); + ctx.fillStyle = `rgb(${red}, 50, 50)`; + ctx.fillRect(pixelX, pixelY, cellSize, cellSize); + + // Add warning pattern + ctx.fillStyle = `rgba(255, 255, 255, ${0.3 + intensity * 0.3})`; + ctx.fillRect(pixelX + 5, pixelY + 5, cellSize - 10, cellSize - 10); + + // Add border + ctx.strokeStyle = '#ff6666'; + ctx.lineWidth = 2; + ctx.strokeRect(pixelX, pixelY, cellSize, cellSize); + break; + + case this.EXIT: + // Animated exit square + const exitTime = Date.now() * 0.005; + const exitGlow = (Math.sin(exitTime) + 1) * 0.5; + const green = Math.floor(100 + exitGlow * 155); + ctx.fillStyle = `rgb(50, ${green}, 50)`; + ctx.fillRect(pixelX, pixelY, cellSize, cellSize); + + // Add exit symbol + ctx.fillStyle = '#ffffff'; + ctx.font = `${cellSize * 0.6}px Arial`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('🏁', pixelX + cellSize / 2, pixelY + cellSize / 2); + break; + } + } + } + } +} diff --git a/src/player.js b/src/player.js new file mode 100644 index 0000000..bfb634f --- /dev/null +++ b/src/player.js @@ -0,0 +1,71 @@ +export class Player { + constructor(x, y, size) { + this.x = x; + this.y = y; + this.size = size; + this.color = '#4CAF50'; + this.animationTime = 0; + this.isMoving = false; + this.moveSpeed = 5; // Animation speed multiplier + } + + moveTo(x, y) { + this.x = x; + this.y = y; + this.isMoving = true; + this.animationTime = 0; + } + + update(deltaTime) { + if (this.isMoving) { + this.animationTime += deltaTime * this.moveSpeed; + if (this.animationTime >= 1000) { // 1 second animation + this.isMoving = false; + this.animationTime = 0; + } + } + } + + render(ctx) { + const pixelX = this.x * this.size; + const pixelY = this.y * this.size; + + // Add a subtle pulse animation + const pulse = Math.sin(Date.now() * 0.005) * 0.1 + 1; + const radius = (this.size * 0.3) * pulse; + + // Draw player as a circle with glow effect + ctx.save(); + + // Glow effect + ctx.shadowColor = this.color; + ctx.shadowBlur = 15; + + // Main player circle + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.arc( + pixelX + this.size / 2, + pixelY + this.size / 2, + radius, + 0, + Math.PI * 2 + ); + ctx.fill(); + + // Inner highlight + ctx.shadowBlur = 0; + ctx.fillStyle = '#81C784'; + ctx.beginPath(); + ctx.arc( + pixelX + this.size / 2 - 3, + pixelY + this.size / 2 - 3, + radius * 0.6, + 0, + Math.PI * 2 + ); + ctx.fill(); + + ctx.restore(); + } +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..58fc1ec --- /dev/null +++ b/src/style.css @@ -0,0 +1,156 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: dark light; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; + background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%); +} + +#app { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.game-container { + background: rgba(0, 0, 0, 0.3); + border-radius: 15px; + padding: 2rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +h1 { + font-size: 3.2em; + line-height: 1.1; + margin-bottom: 1rem; + background: linear-gradient(45deg, #4CAF50, #81C784); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 0 2px 10px rgba(76, 175, 80, 0.3); +} + +.game-info { + margin-bottom: 1.5rem; +} + +.game-info p { + margin: 0.5rem 0; + font-size: 1.1em; + opacity: 0.9; +} + +.visual-warning { + color: #ff6b6b !important; + font-weight: bold; + animation: pulse 2s infinite; +} + +.audio-warning { + color: #ffd700 !important; + font-weight: bold; + animation: pulse 2s infinite; +} + +.audio-hint { + color: #87ceeb !important; + font-size: 0.9em; + font-style: italic; + opacity: 0.8; +} + +@keyframes pulse { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 1; } +} + +#gameCanvas { + border: 3px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + background: #1a1a1a; + box-shadow: + 0 0 20px rgba(0, 0, 0, 0.5), + inset 0 0 20px rgba(255, 255, 255, 0.05); + margin: 1rem 0; + display: block; + margin-left: auto; + margin-right: auto; +} + +.controls { + display: flex; + gap: 1rem; + justify-content: center; + margin-top: 1.5rem; +} + +.controls button { + background: linear-gradient(45deg, #4CAF50, #45a049); + color: white; + border: none; + padding: 0.8rem 1.5rem; + border-radius: 25px; + font-size: 1rem; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.controls button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4); + background: linear-gradient(45deg, #45a049, #4CAF50); +} + +.controls button:active { + transform: translateY(0); + box-shadow: 0 2px 10px rgba(76, 175, 80, 0.3); +} + +/* Responsive design */ +@media (max-width: 768px) { + #app { + padding: 1rem; + } + + .game-container { + padding: 1rem; + } + + h1 { + font-size: 2.5em; + } + + #gameCanvas { + max-width: 100%; + height: auto; + } + + .controls { + flex-direction: column; + gap: 0.5rem; + } +}