mirror of
https://github.com/kuhyx/slavic_game_jam.git
synced 2026-07-04 12:03:04 +02:00
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
This commit is contained in:
commit
a91a61a288
26
.github/copilot-instructions.md
vendored
Normal file
26
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Copilot Instructions
|
||||||
|
|
||||||
|
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
|
||||||
|
|
||||||
|
## 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
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -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?
|
||||||
126
README.md
Normal file
126
README.md
Normal file
@ -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.
|
||||||
28
index.html
Normal file
28
index.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Danger Field Game</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div class="game-container">
|
||||||
|
<h1>Danger Field Game</h1>
|
||||||
|
<div class="game-info">
|
||||||
|
<p>Use WASD or Arrow Keys to move</p>
|
||||||
|
<p class="visual-warning">⚠️ Red squares are visually dangerous!</p>
|
||||||
|
<p class="audio-warning">🔊 Listen carefully - directional audio warns of hidden dangers!</p>
|
||||||
|
<p class="audio-hint">🎧 Use headphones for best directional audio experience</p>
|
||||||
|
</div>
|
||||||
|
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="soundToggle">🔊 Sound: ON</button>
|
||||||
|
<button id="vibrationToggle">📳 Vibration: ON</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1026
package-lock.json
generated
Normal file
1026
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
package.json
Normal file
14
package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
209
src/audio.js
Normal file
209
src/audio.js
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
177
src/game.js
Normal file
177
src/game.js
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/input.js
Normal file
79
src/input.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main.js
Normal file
11
src/main.js
Normal file
@ -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();
|
||||||
|
});
|
||||||
224
src/maze.js
Normal file
224
src/maze.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/player.js
Normal file
71
src/player.js
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/style.css
Normal file
156
src/style.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user