Browse Source
- Added GameWindow class to manage game window creation and rendering. - Integrated SDL2 for window management, rendering, and audio playback. - Implemented methods for loading images, creating textures, and drawing graphics. - Added font management for dynamic text rendering. - Included input handling for keyboard, mouse, and joystick events. - Implemented a main game loop to handle updates and rendering. - Added support for special effects like white flash and blood splatter. - Created utility methods for managing game state and performance.pygame-pyodide
22 changed files with 1896 additions and 3182 deletions
Binary file not shown.
@ -1,308 +0,0 @@
|
||||
# Game Profile Manager |
||||
|
||||
A PySDL2-based user profile management system designed for gamepad-only control with virtual keyboard input. This system allows players to create, edit, delete, and select user profiles for games using only gamepad inputs or directional keys, with no need for physical keyboard text input. |
||||
|
||||
## Features |
||||
|
||||
- **640x480 Resolution**: Optimized for retro gaming systems and handheld devices |
||||
- **Create New Profiles**: Add new user profiles with custom names using virtual keyboard |
||||
- **Profile Selection**: Browse and select active profiles |
||||
- **Edit Settings**: Modify profile settings including difficulty, volume levels, and preferences |
||||
- **Delete Profiles**: Remove unwanted profiles |
||||
- **Gamepad/Directional Navigation**: Full control using only gamepad/joystick inputs or arrow keys |
||||
- **Virtual Keyboard**: Text input using directional controls - no physical keyboard typing required |
||||
- **JSON Storage**: Profiles stored in human-readable JSON format |
||||
- **Persistent Settings**: All changes automatically saved |
||||
|
||||
## Installation |
||||
|
||||
### Requirements |
||||
- Python 3.6+ |
||||
- PySDL2 |
||||
- SDL2 library |
||||
|
||||
### Setup |
||||
```bash |
||||
# Install required Python packages |
||||
pip install pysdl2 |
||||
|
||||
# For Ubuntu/Debian users, you may also need: |
||||
sudo apt-get install libsdl2-dev libsdl2-ttf-dev |
||||
|
||||
# Make launcher executable |
||||
chmod +x launch_profile_manager.sh |
||||
``` |
||||
|
||||
## Usage |
||||
|
||||
### Running the Profile Manager |
||||
```bash |
||||
# Method 1: Use the launcher script |
||||
./launch_profile_manager.sh |
||||
|
||||
# Method 2: Run directly with Python |
||||
python3 profile_manager.py |
||||
``` |
||||
|
||||
### Gamepad Controls |
||||
|
||||
#### Standard Gamepad Layout (Xbox/PlayStation compatible) |
||||
- **D-Pad/Hat**: Navigate menus up/down/left/right, control virtual keyboard cursor |
||||
- **Button 0 (A/X)**: Confirm selection, enter menus, select virtual keyboard characters |
||||
- **Button 1 (B/Circle)**: Go back, cancel action |
||||
- **Button 2 (X/Square)**: Delete profile, backspace in virtual keyboard |
||||
- **Button 3 (Y/Triangle)**: Reserved for future features |
||||
|
||||
#### Keyboard Controls (Alternative) |
||||
- **Arrow Keys**: Navigate menus and virtual keyboard cursor |
||||
- **Enter/Space**: Confirm selection, select virtual keyboard characters |
||||
- **Escape**: Go back, cancel action |
||||
- **Delete/Backspace**: Delete profile, backspace in virtual keyboard |
||||
- **Tab**: Reserved for future features |
||||
|
||||
#### Virtual Keyboard Text Input |
||||
When creating or editing profile names: |
||||
1. **Navigate**: Use D-Pad/Arrow Keys to move cursor over virtual keyboard |
||||
2. **Select Character**: Press A/Enter to add character to profile name |
||||
3. **Backspace**: Press X/Delete to remove last character |
||||
4. **Complete**: Navigate to "DONE" and press A/Enter to finish input |
||||
5. **Cancel**: Navigate to "CANCEL" and press A/Enter to abort |
||||
|
||||
#### Navigation Flow |
||||
1. **Main Menu**: Create Profile → Select Profile → Edit Settings → Exit |
||||
2. **Profile List**: Choose from existing profiles, or go back |
||||
3. **Create Profile**: Use virtual keyboard to enter name, confirm with directional controls |
||||
4. **Edit Profile**: Adjust settings using left/right navigation |
||||
|
||||
### Display Specifications |
||||
- **Resolution**: 640x480 pixels (4:3 aspect ratio) |
||||
- **Optimized for**: Retro gaming systems, handheld devices, embedded systems |
||||
- **Font Scaling**: Adaptive font sizes for optimal readability at low resolution |
||||
|
||||
### Profile Structure |
||||
|
||||
Profiles are stored in `user_profiles.json` with the following structure: |
||||
|
||||
```json |
||||
{ |
||||
"profiles": { |
||||
"PlayerName": { |
||||
"name": "PlayerName", |
||||
"created_date": "2024-01-15T10:30:00", |
||||
"last_played": "2024-01-20T14:45:00", |
||||
"games_played": 25, |
||||
"total_score": 15420, |
||||
"best_score": 980, |
||||
"settings": { |
||||
"difficulty": "normal", |
||||
"sound_volume": 75, |
||||
"music_volume": 60, |
||||
"screen_shake": true, |
||||
"auto_save": true |
||||
}, |
||||
"achievements": [ |
||||
"first_win", |
||||
"score_500" |
||||
] |
||||
} |
||||
}, |
||||
"active_profile": "PlayerName" |
||||
} |
||||
``` |
||||
|
||||
## Integration with Games |
||||
|
||||
### Loading Active Profile |
||||
```python |
||||
import json |
||||
|
||||
def load_active_profile(): |
||||
try: |
||||
with open('user_profiles.json', 'r') as f: |
||||
data = json.load(f) |
||||
active_name = data.get('active_profile') |
||||
if active_name and active_name in data['profiles']: |
||||
return data['profiles'][active_name] |
||||
except (FileNotFoundError, json.JSONDecodeError): |
||||
pass |
||||
return None |
||||
|
||||
# Usage in your game |
||||
profile = load_active_profile() |
||||
if profile: |
||||
difficulty = profile['settings']['difficulty'] |
||||
sound_volume = profile['settings']['sound_volume'] |
||||
``` |
||||
|
||||
### Updating Profile Stats |
||||
```python |
||||
def update_profile_stats(score, game_completed=True): |
||||
try: |
||||
with open('user_profiles.json', 'r') as f: |
||||
data = json.load(f) |
||||
|
||||
active_name = data.get('active_profile') |
||||
if active_name and active_name in data['profiles']: |
||||
profile = data['profiles'][active_name] |
||||
|
||||
if game_completed: |
||||
profile['games_played'] += 1 |
||||
profile['total_score'] += score |
||||
profile['best_score'] = max(profile['best_score'], score) |
||||
profile['last_played'] = datetime.now().isoformat() |
||||
|
||||
with open('user_profiles.json', 'w') as f: |
||||
json.dump(data, f, indent=2) |
||||
except Exception as e: |
||||
print(f"Error updating profile: {e}") |
||||
``` |
||||
|
||||
## Customization |
||||
|
||||
### Adding New Settings |
||||
Edit the `UserProfile` dataclass and the settings adjustment methods: |
||||
|
||||
```python |
||||
# In profile_manager.py, modify the UserProfile.__post_init__ method |
||||
def __post_init__(self): |
||||
if self.settings is None: |
||||
self.settings = { |
||||
"difficulty": "normal", |
||||
"sound_volume": 50, |
||||
"music_volume": 50, |
||||
"screen_shake": True, |
||||
"auto_save": True, |
||||
"your_new_setting": "default_value" # Add here |
||||
} |
||||
``` |
||||
|
||||
### Custom Font |
||||
Place your font file in the `assets/` directory and update the font path: |
||||
```python |
||||
font_path = "assets/your_font.ttf" |
||||
``` |
||||
|
||||
### Screen Resolution |
||||
The application is optimized for 640x480 resolution. To change resolution, modify the window size in the init_sdl method: |
||||
```python |
||||
self.window = sdl2.ext.Window( |
||||
title="Profile Manager", |
||||
size=(your_width, your_height) # Change from (640, 480) |
||||
) |
||||
``` |
||||
|
||||
### Virtual Keyboard Layout |
||||
Customize the virtual keyboard characters by modifying the keyboard_chars list: |
||||
```python |
||||
self.keyboard_chars = [ |
||||
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'], |
||||
['K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T'], |
||||
['U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4'], |
||||
['5', '6', '7', '8', '9', '0', '_', '-', ' ', '<'], |
||||
['DONE', 'CANCEL', '', '', '', '', '', '', '', ''] |
||||
] |
||||
``` |
||||
|
||||
## Troubleshooting |
||||
|
||||
### No Gamepad Detected |
||||
- Ensure your gamepad is connected before starting the application |
||||
- Try different USB ports |
||||
- Check if your gamepad is recognized by your system |
||||
- The application will show "No gamepad detected - using keyboard fallback" |
||||
- Virtual keyboard works with both gamepad and keyboard controls |
||||
|
||||
### Font Issues |
||||
- Ensure the font file exists in the assets directory |
||||
- The system will fall back to default font if custom font is not found |
||||
- Supported font formats: TTF, OTF |
||||
- Font sizes are automatically scaled for 640x480 resolution |
||||
|
||||
### Virtual Keyboard Not Responding |
||||
- Ensure you're in text input mode (creating/editing profile names) |
||||
- Use arrow keys or D-Pad to navigate the virtual keyboard cursor |
||||
- Press Enter/A button to select characters |
||||
- The virtual keyboard cursor should be visible as a highlighted character |
||||
|
||||
### Profile Not Saving |
||||
- Check file permissions in the application directory |
||||
- Ensure sufficient disk space |
||||
- Verify JSON format is not corrupted |
||||
|
||||
### Resolution Issues |
||||
- The application is designed for 640x480 resolution |
||||
- On higher resolution displays, the window may appear small |
||||
- This is intentional for compatibility with retro gaming systems |
||||
- Content is optimized and readable at this resolution |
||||
|
||||
## File Structure |
||||
``` |
||||
project_directory/ |
||||
├── profile_manager.py # Main application (640x480, virtual keyboard) |
||||
├── launch_profile_manager.sh # Launcher script |
||||
├── user_profiles.json # Profile data storage |
||||
├── test_profile_manager.py # Test suite for core functions |
||||
├── game_profile_integration.py # Example game integration |
||||
├── assets/ |
||||
│ └── decterm.ttf # Font file (optional) |
||||
└── README_PROFILE_MANAGER.md # This documentation |
||||
``` |
||||
|
||||
## Development Notes |
||||
|
||||
### Virtual Keyboard Implementation |
||||
The virtual keyboard is implemented as a 2D grid of characters: |
||||
- Cursor position tracked with (keyboard_cursor_x, keyboard_cursor_y) |
||||
- Character selection adds to input_text string |
||||
- Special functions: DONE (confirm), CANCEL (abort), < (backspace) |
||||
- Fully navigable with directional controls only |
||||
|
||||
### Screen Layout for 640x480 |
||||
- Header area: 0-80px (titles, status) |
||||
- Content area: 80-400px (main UI elements) |
||||
- Controls area: 400-480px (help text, instructions) |
||||
- All elements scaled and positioned for optimal readability |
||||
|
||||
### Adding New Screens |
||||
1. Add screen name to `current_screen` handling |
||||
2. Create render method (e.g., `render_new_screen()`) |
||||
3. Add navigation logic in input handlers |
||||
4. Update screen transitions in confirm/back handlers |
||||
|
||||
### Gamepad Button Mapping |
||||
The application uses SDL2's joystick interface. Button numbers may vary by controller: |
||||
- Most modern controllers follow the Xbox layout |
||||
- PlayStation controllers map similarly but may have different button numbers |
||||
- Test with your specific controller and adjust mappings if needed |
||||
|
||||
### Performance Considerations |
||||
- Rendering is capped at 60 FPS for smooth operation |
||||
- Input debouncing prevents accidental rapid inputs |
||||
- JSON operations are minimized and occur only when necessary |
||||
- Virtual keyboard rendering optimized for 640x480 resolution |
||||
- Font scaling automatically adjusted for readability |
||||
|
||||
### Adding Support for Different Resolutions |
||||
To support different screen resolutions, modify these key areas: |
||||
1. Window initialization in `init_sdl()` |
||||
2. Panel and button positioning in render methods |
||||
3. Font size scaling factors |
||||
4. Virtual keyboard grid positioning |
||||
|
||||
### Gamepad Integration Notes |
||||
- Uses SDL2's joystick interface for maximum compatibility |
||||
- Button mapping follows standard Xbox controller layout |
||||
- Hat/D-Pad input prioritized over analog sticks for precision |
||||
- Input timing designed for responsive but not accidental activation |
||||
|
||||
## Target Platforms |
||||
|
||||
This profile manager is specifically designed for: |
||||
- **Handheld Gaming Devices**: Steam Deck, ROG Ally, etc. |
||||
- **Retro Gaming Systems**: RetroPie, Batocera, etc. |
||||
- **Embedded Gaming Systems**: Custom arcade cabinets, portable devices |
||||
- **Low-Resolution Displays**: 640x480, 800x600, and similar resolutions |
||||
- **Gamepad-Only Environments**: Systems without keyboard access |
||||
|
||||
## License |
||||
This profile manager is provided as-is for educational and personal use. Designed for integration with retro and handheld gaming systems. |
||||
@ -0,0 +1,195 @@
|
||||
[ |
||||
"assets/Rat/BMP_1_CAVE_DOWN.png", |
||||
"assets/Rat/BMP_1_CAVE_LEFT.png", |
||||
"assets/Rat/BMP_1_CAVE_RIGHT.png", |
||||
"assets/Rat/BMP_1_CAVE_UP.png", |
||||
"assets/Rat/BMP_1_E.png", |
||||
"assets/Rat/BMP_1_EN.png", |
||||
"assets/Rat/BMP_1_ES.png", |
||||
"assets/Rat/BMP_1_EXPLOSION_DOWN.png", |
||||
"assets/Rat/BMP_1_EXPLOSION_LEFT.png", |
||||
"assets/Rat/BMP_1_EXPLOSION_RIGHT.png", |
||||
"assets/Rat/BMP_1_EXPLOSION_UP.png", |
||||
"assets/Rat/BMP_1_FLOWER_1.png", |
||||
"assets/Rat/BMP_1_FLOWER_2.png", |
||||
"assets/Rat/BMP_1_FLOWER_3.png", |
||||
"assets/Rat/BMP_1_FLOWER_4.png", |
||||
"assets/Rat/BMP_1_GAS_DOWN.png", |
||||
"assets/Rat/BMP_1_GAS_LEFT.png", |
||||
"assets/Rat/BMP_1_GAS_RIGHT.png", |
||||
"assets/Rat/BMP_1_GAS_UP.png", |
||||
"assets/Rat/BMP_1_GRASS_1.png", |
||||
"assets/Rat/BMP_1_GRASS_2.png", |
||||
"assets/Rat/BMP_1_GRASS_3.png", |
||||
"assets/Rat/BMP_1_GRASS_4.png", |
||||
"assets/Rat/BMP_1_N.png", |
||||
"assets/Rat/BMP_1_NE.png", |
||||
"assets/Rat/BMP_1_NW.png", |
||||
"assets/Rat/BMP_1_S.png", |
||||
"assets/Rat/BMP_1_SE.png", |
||||
"assets/Rat/BMP_1_SW.png", |
||||
"assets/Rat/BMP_1_W.png", |
||||
"assets/Rat/BMP_1_WN.png", |
||||
"assets/Rat/BMP_1_WS.png", |
||||
"assets/Rat/BMP_2_CAVE_DOWN.png", |
||||
"assets/Rat/BMP_2_CAVE_LEFT.png", |
||||
"assets/Rat/BMP_2_CAVE_RIGHT.png", |
||||
"assets/Rat/BMP_2_CAVE_UP.png", |
||||
"assets/Rat/BMP_2_E.png", |
||||
"assets/Rat/BMP_2_EN.png", |
||||
"assets/Rat/BMP_2_ES.png", |
||||
"assets/Rat/BMP_2_EXPLOSION_DOWN.png", |
||||
"assets/Rat/BMP_2_EXPLOSION_LEFT.png", |
||||
"assets/Rat/BMP_2_EXPLOSION_RIGHT.png", |
||||
"assets/Rat/BMP_2_EXPLOSION_UP.png", |
||||
"assets/Rat/BMP_2_FLOWER_1.png", |
||||
"assets/Rat/BMP_2_FLOWER_2.png", |
||||
"assets/Rat/BMP_2_FLOWER_3.png", |
||||
"assets/Rat/BMP_2_FLOWER_4.png", |
||||
"assets/Rat/BMP_2_GAS_DOWN.png", |
||||
"assets/Rat/BMP_2_GAS_LEFT.png", |
||||
"assets/Rat/BMP_2_GAS_RIGHT.png", |
||||
"assets/Rat/BMP_2_GAS_UP.png", |
||||
"assets/Rat/BMP_2_GRASS_1.png", |
||||
"assets/Rat/BMP_2_GRASS_2.png", |
||||
"assets/Rat/BMP_2_GRASS_3.png", |
||||
"assets/Rat/BMP_2_GRASS_4.png", |
||||
"assets/Rat/BMP_2_N.png", |
||||
"assets/Rat/BMP_2_NE.png", |
||||
"assets/Rat/BMP_2_NW.png", |
||||
"assets/Rat/BMP_2_S.png", |
||||
"assets/Rat/BMP_2_SE.png", |
||||
"assets/Rat/BMP_2_SW.png", |
||||
"assets/Rat/BMP_2_W.png", |
||||
"assets/Rat/BMP_2_WN.png", |
||||
"assets/Rat/BMP_2_WS.png", |
||||
"assets/Rat/BMP_3_CAVE_DOWN.png", |
||||
"assets/Rat/BMP_3_CAVE_LEFT.png", |
||||
"assets/Rat/BMP_3_CAVE_RIGHT.png", |
||||
"assets/Rat/BMP_3_CAVE_UP.png", |
||||
"assets/Rat/BMP_3_E.png", |
||||
"assets/Rat/BMP_3_EN.png", |
||||
"assets/Rat/BMP_3_ES.png", |
||||
"assets/Rat/BMP_3_EXPLOSION_DOWN.png", |
||||
"assets/Rat/BMP_3_EXPLOSION_LEFT.png", |
||||
"assets/Rat/BMP_3_EXPLOSION_RIGHT.png", |
||||
"assets/Rat/BMP_3_EXPLOSION_UP.png", |
||||
"assets/Rat/BMP_3_FLOWER_1.png", |
||||
"assets/Rat/BMP_3_FLOWER_2.png", |
||||
"assets/Rat/BMP_3_FLOWER_3.png", |
||||
"assets/Rat/BMP_3_FLOWER_4.png", |
||||
"assets/Rat/BMP_3_GAS_DOWN.png", |
||||
"assets/Rat/BMP_3_GAS_LEFT.png", |
||||
"assets/Rat/BMP_3_GAS_RIGHT.png", |
||||
"assets/Rat/BMP_3_GAS_UP.png", |
||||
"assets/Rat/BMP_3_GRASS_1.png", |
||||
"assets/Rat/BMP_3_GRASS_2.png", |
||||
"assets/Rat/BMP_3_GRASS_3.png", |
||||
"assets/Rat/BMP_3_GRASS_4.png", |
||||
"assets/Rat/BMP_3_N.png", |
||||
"assets/Rat/BMP_3_NE.png", |
||||
"assets/Rat/BMP_3_NW.png", |
||||
"assets/Rat/BMP_3_S.png", |
||||
"assets/Rat/BMP_3_SE.png", |
||||
"assets/Rat/BMP_3_SW.png", |
||||
"assets/Rat/BMP_3_W.png", |
||||
"assets/Rat/BMP_3_WN.png", |
||||
"assets/Rat/BMP_3_WS.png", |
||||
"assets/Rat/BMP_4_CAVE_DOWN.png", |
||||
"assets/Rat/BMP_4_CAVE_LEFT.png", |
||||
"assets/Rat/BMP_4_CAVE_RIGHT.png", |
||||
"assets/Rat/BMP_4_CAVE_UP.png", |
||||
"assets/Rat/BMP_4_E.png", |
||||
"assets/Rat/BMP_4_EN.png", |
||||
"assets/Rat/BMP_4_ES.png", |
||||
"assets/Rat/BMP_4_EXPLOSION_DOWN.png", |
||||
"assets/Rat/BMP_4_EXPLOSION_LEFT.png", |
||||
"assets/Rat/BMP_4_EXPLOSION_RIGHT.png", |
||||
"assets/Rat/BMP_4_EXPLOSION_UP.png", |
||||
"assets/Rat/BMP_4_FLOWER_1.png", |
||||
"assets/Rat/BMP_4_FLOWER_2.png", |
||||
"assets/Rat/BMP_4_FLOWER_3.png", |
||||
"assets/Rat/BMP_4_FLOWER_4.png", |
||||
"assets/Rat/BMP_4_GAS_DOWN.png", |
||||
"assets/Rat/BMP_4_GAS_LEFT.png", |
||||
"assets/Rat/BMP_4_GAS_RIGHT.png", |
||||
"assets/Rat/BMP_4_GAS_UP.png", |
||||
"assets/Rat/BMP_4_GRASS_1.png", |
||||
"assets/Rat/BMP_4_GRASS_2.png", |
||||
"assets/Rat/BMP_4_GRASS_3.png", |
||||
"assets/Rat/BMP_4_GRASS_4.png", |
||||
"assets/Rat/BMP_4_N.png", |
||||
"assets/Rat/BMP_4_NE.png", |
||||
"assets/Rat/BMP_4_NW.png", |
||||
"assets/Rat/BMP_4_S.png", |
||||
"assets/Rat/BMP_4_SE.png", |
||||
"assets/Rat/BMP_4_SW.png", |
||||
"assets/Rat/BMP_4_W.png", |
||||
"assets/Rat/BMP_4_WN.png", |
||||
"assets/Rat/BMP_4_WS.png", |
||||
"assets/Rat/BMP_ARROW_DOWN.png", |
||||
"assets/Rat/BMP_ARROW_LEFT.png", |
||||
"assets/Rat/BMP_ARROW_RIGHT.png", |
||||
"assets/Rat/BMP_ARROW_UP.png", |
||||
"assets/Rat/BMP_BABY_DOWN.png", |
||||
"assets/Rat/BMP_BABY_LEFT.png", |
||||
"assets/Rat/BMP_BABY_RIGHT.png", |
||||
"assets/Rat/BMP_BABY_UP.png", |
||||
"assets/Rat/BMP_BLOCK_0.png", |
||||
"assets/Rat/BMP_BLOCK_1.png", |
||||
"assets/Rat/BMP_BLOCK_2.png", |
||||
"assets/Rat/BMP_BLOCK_3.png", |
||||
"assets/Rat/BMP_BOMB0.png", |
||||
"assets/Rat/BMP_BOMB1.png", |
||||
"assets/Rat/BMP_BOMB2.png", |
||||
"assets/Rat/BMP_BOMB3.png", |
||||
"assets/Rat/BMP_BOMB4.png", |
||||
"assets/Rat/BMP_BONUS_10.png", |
||||
"assets/Rat/BMP_BONUS_160.png", |
||||
"assets/Rat/BMP_BONUS_20.png", |
||||
"assets/Rat/BMP_BONUS_40.png", |
||||
"assets/Rat/BMP_BONUS_5.png", |
||||
"assets/Rat/BMP_BONUS_80.png", |
||||
"assets/Rat/BMP_EXPLOSION.png", |
||||
"assets/Rat/BMP_EXPLOSION_DOWN.png", |
||||
"assets/Rat/BMP_EXPLOSION_LEFT.png", |
||||
"assets/Rat/BMP_EXPLOSION_RIGHT.png", |
||||
"assets/Rat/BMP_EXPLOSION_UP.png", |
||||
"assets/Rat/BMP_FEMALE.png", |
||||
"assets/Rat/BMP_FEMALE_DOWN.png", |
||||
"assets/Rat/BMP_FEMALE_LEFT.png", |
||||
"assets/Rat/BMP_FEMALE_RIGHT.png", |
||||
"assets/Rat/BMP_FEMALE_UP.png", |
||||
"assets/Rat/BMP_GAS.png", |
||||
"assets/Rat/BMP_GAS_DOWN.png", |
||||
"assets/Rat/BMP_GAS_LEFT.png", |
||||
"assets/Rat/BMP_GAS_RIGHT.png", |
||||
"assets/Rat/BMP_GAS_UP.png", |
||||
"assets/Rat/BMP_MALE.png", |
||||
"assets/Rat/BMP_MALE_DOWN.png", |
||||
"assets/Rat/BMP_MALE_LEFT.png", |
||||
"assets/Rat/BMP_MALE_RIGHT.png", |
||||
"assets/Rat/BMP_MALE_UP.png", |
||||
"assets/Rat/BMP_NUCLEAR.png", |
||||
"assets/Rat/BMP_POISON.png", |
||||
"assets/Rat/BMP_START_1.png", |
||||
"assets/Rat/BMP_START_1_DOWN.png", |
||||
"assets/Rat/BMP_START_1_SHADED.png", |
||||
"assets/Rat/BMP_START_2.png", |
||||
"assets/Rat/BMP_START_2_DOWN.png", |
||||
"assets/Rat/BMP_START_2_SHADED.png", |
||||
"assets/Rat/BMP_START_3.png", |
||||
"assets/Rat/BMP_START_3_DOWN.png", |
||||
"assets/Rat/BMP_START_3_SHADED.png", |
||||
"assets/Rat/BMP_START_4.png", |
||||
"assets/Rat/BMP_START_4_DOWN.png", |
||||
"assets/Rat/BMP_START_4_SHADED.png", |
||||
"assets/Rat/BMP_TITLE.png", |
||||
"assets/Rat/BMP_TUNNEL.png", |
||||
"assets/Rat/BMP_VERMINATORS.png", |
||||
"assets/Rat/BMP_WEWIN.png", |
||||
"assets/Rat/mine.png", |
||||
"assets/decterm.ttf", |
||||
"assets/AmaticSC-Regular.ttf", |
||||
"assets/terminal.ttf" |
||||
] |
||||
@ -0,0 +1,21 @@
|
||||
[ |
||||
"sound/BIRTH.WAV", |
||||
"sound/BOMB.WAV", |
||||
"sound/CHOKE.WAV", |
||||
"sound/CLUNK.WAV", |
||||
"sound/Death.wav", |
||||
"sound/GAS.WAV", |
||||
"sound/NEWSEX.WAV", |
||||
"sound/NUCLEAR.WAV", |
||||
"sound/POISON.WAV", |
||||
"sound/PUTDOWN.WAV", |
||||
"sound/SEX.WAV", |
||||
"sound/VICTORY.WAV", |
||||
"sound/WELLDONE.WAV", |
||||
"sound/WEWIN.WAV", |
||||
"sound/converted_BOMB.wav", |
||||
"sound/mine.wav", |
||||
"sound/mine_converted.wav", |
||||
"sound/mine_original.wav", |
||||
"sound/nuke.wav" |
||||
] |
||||
@ -1,33 +0,0 @@
|
||||
{ |
||||
"keybinding_game": { |
||||
"keydown_Return": "spawn_rat", |
||||
"keydown_D": "kill_rat", |
||||
"keydown_M": "toggle_audio", |
||||
"keydown_F": "toggle_full_screen", |
||||
"keydown_Up": "start_scrolling|Up", |
||||
"keydown_Down": "start_scrolling|Down", |
||||
"keydown_Left": "start_scrolling|Left", |
||||
"keydown_Right": "start_scrolling|Right", |
||||
"keyup_Up": "stop_scrolling", |
||||
"keyup_Down": "stop_scrolling", |
||||
"keyup_Left": "stop_scrolling", |
||||
"keyup_Right": "stop_scrolling", |
||||
"keydown_Space": "spawn_new_bomb", |
||||
"keydown_N": "spawn_new_nuclear_bomb", |
||||
"keydown_Left_Ctrl": "spawn_new_mine", |
||||
"keydown_G": "spawn_gas", |
||||
"keydown_P": "toggle_pause" |
||||
}, |
||||
"keybinding_start_menu": { |
||||
"keydown_Return": "reset_game", |
||||
"keydown_Escape": "quit_game", |
||||
"keydown_M": "toggle_audio", |
||||
"keydown_F": "toggle_full_screen" |
||||
}, |
||||
"keybinding_paused": { |
||||
"keydown_Return": "reset_game", |
||||
"keydown_Escape": "quit_game", |
||||
"keydown_M": "toggle_audio", |
||||
"keydown_F": "toggle_full_screen" |
||||
} |
||||
} |
||||
@ -1,29 +0,0 @@
|
||||
keybinding_game: |
||||
keydown_Return: spawn_rat |
||||
keydown_D: kill_rat |
||||
keydown_M: toggle_audio |
||||
keydown_F: toggle_full_screen |
||||
keydown_Up: start_scrolling|Up |
||||
keydown_Down: start_scrolling|Down |
||||
keydown_Left: start_scrolling|Left |
||||
keydown_Right: start_scrolling|Right |
||||
keyup_Up: stop_scrolling |
||||
keyup_Down: stop_scrolling |
||||
keyup_Left: stop_scrolling |
||||
keyup_Right: stop_scrolling |
||||
keydown_Space: spawn_new_bomb |
||||
keydown_N: spawn_new_nuclear_bomb |
||||
keydown_Left_Ctrl: spawn_new_mine |
||||
keydown_P: toggle_pause |
||||
|
||||
keybinding_start_menu: |
||||
keydown_Return: reset_game |
||||
keydown_Escape: quit_game |
||||
keydown_M: toggle_audio |
||||
keydown_F: toggle_full_screen |
||||
|
||||
keybinding_paused: |
||||
keydown_Return: reset_game |
||||
keydown_Escape: quit_game |
||||
keydown_M: toggle_audio |
||||
keydown_F: toggle_full_screen |
||||
@ -0,0 +1,833 @@
|
||||
import os |
||||
import random |
||||
import pygame |
||||
from pygame import mixer |
||||
|
||||
|
||||
class GameWindow: |
||||
""" |
||||
Pygame-based game window implementation. |
||||
Provides a complete interface equivalent to sdl2_layer.GameWindow |
||||
""" |
||||
|
||||
def __init__(self, width, height, cell_size, title="Default", key_callback=None): |
||||
# Display configuration |
||||
self.cell_size = cell_size |
||||
self.width = width * cell_size |
||||
self.height = height * cell_size |
||||
|
||||
# Screen resolution handling |
||||
actual_screen_size = os.environ.get("RESOLUTION", "640x480").split("x") |
||||
actual_screen_size = tuple(map(int, actual_screen_size)) |
||||
self.target_size = actual_screen_size if self.width > actual_screen_size[0] or self.height > actual_screen_size[1] else (self.width, self.height) |
||||
|
||||
# View offset calculations |
||||
self.w_start_offset = (self.target_size[0] - self.width) // 2 |
||||
self.h_start_offset = (self.target_size[1] - self.height) // 2 |
||||
self.w_offset = self.w_start_offset |
||||
self.h_offset = self.h_start_offset |
||||
self.max_w_offset = self.target_size[0] - self.width |
||||
self.max_h_offset = self.target_size[1] - self.height |
||||
self.scale = self.target_size[1] // self.cell_size |
||||
|
||||
print(f"Screen size: {self.width}x{self.height}") |
||||
|
||||
# Pygame initialization |
||||
pygame.init() |
||||
mixer.init(frequency=22050, size=-16, channels=1, buffer=2048) |
||||
|
||||
# Window and screen setup |
||||
self.window = pygame.display.set_mode(self.target_size) |
||||
pygame.display.set_caption(title) |
||||
self.screen = self.window |
||||
|
||||
# Font system |
||||
self.fonts = self.generate_fonts("assets/decterm.ttf") |
||||
|
||||
# Game state |
||||
self.running = True |
||||
self.delay = 30 |
||||
self.performance = 0 |
||||
self.last_status_text = "" |
||||
self.stats_sprite = None |
||||
self.mean_fps = 0 |
||||
self.fpss = [] |
||||
self.text_width = 0 |
||||
self.text_height = 0 |
||||
self.ammo_text = "" |
||||
self.stats_background = None |
||||
self.ammo_background = None |
||||
self.ammo_sprite = None |
||||
|
||||
# White flash effect state |
||||
self.white_flash_active = False |
||||
self.white_flash_start_time = 0 |
||||
self.white_flash_opacity = 255 |
||||
|
||||
# Input handling |
||||
self.trigger = key_callback |
||||
self.button_cursor = [0, 0] |
||||
self.buttons = {} |
||||
|
||||
# Audio system initialization |
||||
self._init_audio_system() |
||||
self.audio = True |
||||
|
||||
# Clock for frame rate control |
||||
self.clock = pygame.time.Clock() |
||||
|
||||
# Input devices |
||||
self.load_joystick() |
||||
|
||||
def show(self): |
||||
"""Show the window (for compatibility with SDL2 interface)""" |
||||
pygame.display.set_mode(self.target_size) |
||||
|
||||
def _init_audio_system(self): |
||||
"""Initialize audio channels for different audio types""" |
||||
mixer.set_num_channels(8) # Ensure enough channels |
||||
self.audio_channels = { |
||||
"base": mixer.Channel(0), |
||||
"effects": mixer.Channel(1), |
||||
"music": mixer.Channel(2) |
||||
} |
||||
self.current_sounds = {} |
||||
|
||||
# ====================== |
||||
# TEXTURE & IMAGE METHODS |
||||
# ====================== |
||||
|
||||
def create_texture(self, tiles: list): |
||||
"""Create a texture from a list of tiles""" |
||||
bg_surface = pygame.Surface((self.width, self.height)) |
||||
for tile in tiles: |
||||
bg_surface.blit(tile[0], (tile[1], tile[2])) |
||||
return bg_surface |
||||
|
||||
# Helpers to support incremental background generation |
||||
def create_empty_background_surface(self): |
||||
"""Create and return an empty background surface to incrementally blit onto.""" |
||||
return pygame.Surface((self.width, self.height)) |
||||
|
||||
def blit_tiles_batch(self, bg_surface, tiles_batch: list): |
||||
"""Blit a small batch of tiles onto the provided background surface. |
||||
|
||||
tiles_batch: list of (surface, x, y) |
||||
Returns None. Designed to be called repeatedly with small batches to avoid long blocking operations. |
||||
""" |
||||
for tile, x, y in tiles_batch: |
||||
try: |
||||
bg_surface.blit(tile, (x, y)) |
||||
except Exception: |
||||
# If tile is a SpriteWrapper, extract surface |
||||
try: |
||||
bg_surface.blit(tile.surface, (x, y)) |
||||
except Exception: |
||||
pass |
||||
|
||||
def load_image(self, path, transparent_color=None, surface=False): |
||||
"""Load and process an image with optional transparency and scaling""" |
||||
image_path = os.path.join("assets", path) |
||||
|
||||
# First try to use pygame's native loader which avoids a Pillow dependency. |
||||
try: |
||||
py_image = pygame.image.load(image_path) |
||||
# Ensure alpha if needed |
||||
try: |
||||
py_image = py_image.convert_alpha() |
||||
except Exception: |
||||
try: |
||||
py_image = py_image.convert() |
||||
except Exception: |
||||
pass |
||||
|
||||
# Handle transparent color via colorkey if provided |
||||
if transparent_color: |
||||
# pygame expects a tuple of ints |
||||
try: |
||||
py_image.set_colorkey(transparent_color) |
||||
except Exception: |
||||
pass |
||||
|
||||
# Scale image using pygame transforms |
||||
scale = max(1, self.cell_size // 20) |
||||
new_size = (py_image.get_width() * scale, py_image.get_height() * scale) |
||||
try: |
||||
py_image = pygame.transform.scale(py_image, new_size) |
||||
except Exception: |
||||
# If scaling fails, continue with original |
||||
pass |
||||
|
||||
if not surface: |
||||
return SpriteWrapper(py_image) |
||||
return py_image |
||||
except Exception: |
||||
# Fallback to PIL-based loading if pygame can't handle the file or Pillow is present |
||||
try: |
||||
# Import Pillow lazily to avoid hard dependency at module import time |
||||
try: |
||||
from PIL import Image |
||||
except Exception: |
||||
Image = None |
||||
|
||||
if Image is None: |
||||
raise |
||||
|
||||
image = Image.open(image_path) |
||||
|
||||
# Handle transparency |
||||
if transparent_color: |
||||
image = image.convert("RGBA") |
||||
datas = image.getdata() |
||||
new_data = [] |
||||
for item in datas: |
||||
if item[:3] == transparent_color: |
||||
new_data.append((255, 255, 255, 0)) |
||||
else: |
||||
new_data.append(item) |
||||
image.putdata(new_data) |
||||
|
||||
# Scale image |
||||
scale = max(1, self.cell_size // 20) |
||||
image = image.resize((image.width * scale, image.height * scale), Image.NEAREST) |
||||
|
||||
# Convert PIL image to pygame surface |
||||
mode = image.mode |
||||
size = image.size |
||||
data = image.tobytes() |
||||
|
||||
if mode == "RGBA": |
||||
py_image = pygame.image.fromstring(data, size, mode) |
||||
elif mode == "RGB": |
||||
py_image = pygame.image.fromstring(data, size, mode) |
||||
else: |
||||
image = image.convert("RGBA") |
||||
data = image.tobytes() |
||||
py_image = pygame.image.fromstring(data, size, "RGBA") |
||||
|
||||
if not surface: |
||||
return SpriteWrapper(py_image) |
||||
return py_image |
||||
except Exception: |
||||
# If both loaders fail, raise to notify caller |
||||
raise |
||||
|
||||
def get_image_size(self, image): |
||||
"""Get the size of an image sprite""" |
||||
if isinstance(image, SpriteWrapper): |
||||
return image.size |
||||
return image.get_size() |
||||
|
||||
# ====================== |
||||
# FONT MANAGEMENT |
||||
# ====================== |
||||
|
||||
def generate_fonts(self, font_file): |
||||
"""Generate font objects for different sizes""" |
||||
fonts = {} |
||||
for i in range(10, 70, 1): |
||||
try: |
||||
fonts[i] = pygame.font.Font(font_file, i) |
||||
except: |
||||
fonts[i] = pygame.font.Font(None, i) |
||||
return fonts |
||||
|
||||
# ====================== |
||||
# DRAWING METHODS |
||||
# ====================== |
||||
|
||||
def draw_text(self, text, font, position, color): |
||||
"""Draw text at specified position with given font and color""" |
||||
if isinstance(color, tuple): |
||||
# Pygame color format |
||||
pass |
||||
else: |
||||
# Convert from any other format to RGB tuple |
||||
color = (color.r, color.g, color.b) if hasattr(color, 'r') else (0, 0, 0) |
||||
|
||||
text_surface = font.render(text, True, color) |
||||
text_rect = text_surface.get_rect() |
||||
|
||||
# Handle center positioning |
||||
if position == "center": |
||||
position = ("center", "center") |
||||
if isinstance(position, tuple): |
||||
if position[0] == "center": |
||||
text_rect.centerx = self.target_size[0] // 2 |
||||
text_rect.y = position[1] |
||||
elif position[1] == "center": |
||||
text_rect.x = position[0] |
||||
text_rect.centery = self.target_size[1] // 2 |
||||
else: |
||||
text_rect.topleft = position |
||||
|
||||
self.screen.blit(text_surface, text_rect) |
||||
|
||||
def draw_background(self, bg_texture): |
||||
"""Draw background texture with current view offset""" |
||||
self.screen.blit(bg_texture, (self.w_offset, self.h_offset)) |
||||
|
||||
def draw_image(self, x, y, sprite, tag=None, anchor="nw"): |
||||
"""Draw an image sprite at specified coordinates""" |
||||
if not self.is_in_visible_area(x, y): |
||||
return |
||||
|
||||
if isinstance(sprite, SpriteWrapper): |
||||
surface = sprite.surface |
||||
else: |
||||
surface = sprite |
||||
|
||||
self.screen.blit(surface, (x + self.w_offset, y + self.h_offset)) |
||||
|
||||
def draw_rectangle(self, x, y, width, height, tag, outline="red", filling=None): |
||||
"""Draw a rectangle with optional fill and outline""" |
||||
rect = pygame.Rect(x, y, width, height) |
||||
|
||||
if filling: |
||||
pygame.draw.rect(self.screen, filling, rect) |
||||
else: |
||||
# Handle outline color |
||||
if isinstance(outline, str): |
||||
color_map = { |
||||
"red": (255, 0, 0), |
||||
"blue": (0, 0, 255), |
||||
"green": (0, 255, 0), |
||||
"black": (0, 0, 0), |
||||
"white": (255, 255, 255) |
||||
} |
||||
outline = color_map.get(outline, (255, 0, 0)) |
||||
pygame.draw.rect(self.screen, outline, rect, 2) |
||||
|
||||
def draw_pointer(self, x, y): |
||||
"""Draw a red pointer rectangle at specified coordinates""" |
||||
x = x + self.w_offset |
||||
y = y + self.h_offset |
||||
for i in range(3): |
||||
rect = pygame.Rect(x + i, y + i, self.cell_size - 2*i, self.cell_size - 2*i) |
||||
pygame.draw.rect(self.screen, (255, 0, 0), rect, 1) |
||||
|
||||
def delete_tag(self, tag): |
||||
"""Placeholder for tag deletion (not needed in pygame implementation)""" |
||||
pass |
||||
|
||||
# ====================== |
||||
# UI METHODS |
||||
# ====================== |
||||
|
||||
def dialog(self, text, **kwargs): |
||||
"""Display a dialog box with text and optional extras""" |
||||
# Draw dialog background |
||||
dialog_rect = pygame.Rect(50, 50, self.target_size[0] - 100, self.target_size[1] - 100) |
||||
pygame.draw.rect(self.screen, (255, 255, 255), dialog_rect) |
||||
|
||||
# Calculate layout positions to avoid overlaps |
||||
title_y = self.target_size[1] // 4 # Title at 1/4 of screen height |
||||
|
||||
# Draw main text (title) |
||||
self.draw_text(text, self.fonts[self.target_size[1]//20], |
||||
("center", title_y), (0, 0, 0)) |
||||
|
||||
# Draw image if provided - position it below title |
||||
image_bottom_y = title_y + 60 # Default position if no image |
||||
if image := kwargs.get("image"): |
||||
image_size = self.get_image_size(image) |
||||
image_y = title_y + 50 |
||||
self.draw_image(self.target_size[0] // 2 - image_size[0] // 2 - self.w_offset, |
||||
image_y - self.h_offset, |
||||
image, "win") |
||||
image_bottom_y = image_y + image_size[1] + 20 |
||||
|
||||
# Draw subtitle if provided - handle multi-line text, position below image |
||||
if subtitle := kwargs.get("subtitle"): |
||||
subtitle_lines = subtitle.split('\n') |
||||
base_y = image_bottom_y + 20 |
||||
line_height = 25 # Fixed line height for consistent spacing |
||||
|
||||
for i, line in enumerate(subtitle_lines): |
||||
if line.strip(): # Only draw non-empty lines |
||||
self.draw_text(line.strip(), self.fonts[self.target_size[1]//35], |
||||
("center", base_y + i * line_height), (0, 0, 0)) |
||||
|
||||
# Draw scores if provided - position at bottom |
||||
if scores := kwargs.get("scores"): |
||||
scores_start_y = self.target_size[1] * 3 // 4 # Bottom quarter of screen |
||||
title_surface = self.fonts[self.target_size[1]//25].render("High Scores:", True, (0, 0, 0)) |
||||
title_rect = title_surface.get_rect(center=(self.target_size[0] // 2, scores_start_y)) |
||||
self.screen.blit(title_surface, title_rect) |
||||
|
||||
for i, score in enumerate(scores[:5]): |
||||
if len(score) >= 4: # New format: date, score, name, device |
||||
score_text = f"{score[2]}: {score[1]} pts ({score[3]})" |
||||
elif len(score) >= 3: # Medium format: date, score, name |
||||
score_text = f"{score[2]}: {score[1]} pts" |
||||
else: # Old format: date, score |
||||
score_text = f"Guest: {score[1]} pts" |
||||
|
||||
self.draw_text(score_text, self.fonts[self.target_size[1]//45], |
||||
("center", scores_start_y + 30 + 25 * (i + 1)), |
||||
(0, 0, 0)) |
||||
|
||||
def start_dialog(self, **kwargs): |
||||
"""Display the welcome dialog""" |
||||
self.dialog("Welcome to the Mice!", subtitle="A game by Matteo because was bored", **kwargs) |
||||
|
||||
def draw_button(self, x, y, text, width, height, coords): |
||||
"""Draw a button with text""" |
||||
color = (0, 0, 255) if self.button_cursor == list(coords) else (0, 0, 0) |
||||
self.draw_rectangle(x, y, width, height, "button", outline=color) |
||||
|
||||
def update_status(self, text): |
||||
"""Update and display the status bar with FPS information""" |
||||
fps = int(self.clock.get_fps()) if self.clock.get_fps() > 0 else 0 |
||||
|
||||
if len(self.fpss) > 20: |
||||
self.mean_fps = round(sum(self.fpss) / len(self.fpss)) if self.fpss else fps |
||||
self.fpss.clear() |
||||
else: |
||||
self.fpss.append(fps) |
||||
|
||||
status_text = f"FPS: {self.mean_fps} - {text}" |
||||
if status_text != self.last_status_text: |
||||
self.last_status_text = status_text |
||||
font = self.fonts[20] |
||||
self.stats_sprite = font.render(status_text, True, (0, 0, 0)) |
||||
if self.text_width != self.stats_sprite.get_width() or self.text_height != self.stats_sprite.get_height(): |
||||
self.text_width, self.text_height = self.stats_sprite.get_size() |
||||
self.stats_background = pygame.Surface((self.text_width + 10, self.text_height + 4)) |
||||
self.stats_background.fill((255, 255, 255)) |
||||
|
||||
self.screen.blit(self.stats_background, (3, 3)) |
||||
self.screen.blit(self.stats_sprite, (8, 5)) |
||||
|
||||
def update_ammo(self, ammo, assets): |
||||
"""Update and display the ammo count""" |
||||
ammo_text = f"{ammo['bomb']['count']}/{ammo['bomb']['max']} {ammo['mine']['count']}/{ammo['mine']['max']} {ammo['gas']['count']}/{ammo['gas']['max']} " |
||||
if self.ammo_text != ammo_text: |
||||
self.ammo_text = ammo_text |
||||
font = self.fonts[20] |
||||
self.ammo_sprite = font.render(ammo_text, True, (0, 0, 0)) |
||||
text_width, text_height = self.ammo_sprite.get_size() |
||||
self.ammo_background = pygame.Surface((text_width + 10, text_height + 4)) |
||||
self.ammo_background.fill((255, 255, 255)) |
||||
|
||||
text_width, text_height = self.ammo_sprite.get_size() |
||||
position = (self.target_size[0] - text_width - 10, self.target_size[1] - text_height - 5) |
||||
|
||||
self.screen.blit(self.ammo_background, (position[0] - 5, position[1] - 2)) |
||||
self.screen.blit(self.ammo_sprite, position) |
||||
|
||||
# Draw ammo icons |
||||
bomb_sprite = assets["BMP_BOMB0"] |
||||
poison_sprite = assets["BMP_POISON"] |
||||
gas_sprite = assets["BMP_GAS"] |
||||
|
||||
if isinstance(bomb_sprite, SpriteWrapper): |
||||
self.screen.blit(bomb_sprite.surface, (position[0]+25, position[1])) |
||||
else: |
||||
# Scale to 20x20 if needed |
||||
bomb_scaled = pygame.transform.scale(bomb_sprite, (20, 20)) |
||||
self.screen.blit(bomb_scaled, (position[0]+25, position[1])) |
||||
|
||||
if isinstance(poison_sprite, SpriteWrapper): |
||||
self.screen.blit(poison_sprite.surface, (position[0]+85, position[1])) |
||||
else: |
||||
poison_scaled = pygame.transform.scale(poison_sprite, (20, 20)) |
||||
self.screen.blit(poison_scaled, (position[0]+85, position[1])) |
||||
|
||||
if isinstance(gas_sprite, SpriteWrapper): |
||||
self.screen.blit(gas_sprite.surface, (position[0]+140, position[1])) |
||||
else: |
||||
gas_scaled = pygame.transform.scale(gas_sprite, (20, 20)) |
||||
self.screen.blit(gas_scaled, (position[0]+140, position[1])) |
||||
|
||||
# ====================== |
||||
# VIEW & NAVIGATION |
||||
# ====================== |
||||
|
||||
def scroll_view(self, pointer): |
||||
"""Adjust the view offset based on pointer coordinates""" |
||||
x, y = pointer |
||||
|
||||
# Scale down and invert coordinates |
||||
x = -(x // 2) * self.cell_size |
||||
y = -(y // 2) * self.cell_size |
||||
|
||||
# Clamp horizontal offset to valid range |
||||
if x <= self.max_w_offset + self.cell_size: |
||||
x = self.max_w_offset |
||||
|
||||
# Clamp vertical offset to valid range |
||||
if y < self.max_h_offset: |
||||
y = self.max_h_offset |
||||
|
||||
self.w_offset = x |
||||
self.h_offset = y |
||||
|
||||
def is_in_visible_area(self, x, y): |
||||
"""Check if coordinates are within the visible area""" |
||||
return (-self.w_offset - self.cell_size <= x <= self.width - self.w_offset and |
||||
-self.h_offset - self.cell_size <= y <= self.height - self.h_offset) |
||||
|
||||
def get_view_center(self): |
||||
"""Get the center coordinates of the current view""" |
||||
return self.w_offset + self.width // 2, self.h_offset + self.height // 2 |
||||
|
||||
# ====================== |
||||
# AUDIO METHODS |
||||
# ====================== |
||||
|
||||
def play_sound(self, sound_file, tag="base"): |
||||
"""Play a sound file on the specified audio channel""" |
||||
if not self.audio: |
||||
return |
||||
|
||||
try: |
||||
sound_path = os.path.join("sound", sound_file) |
||||
sound = mixer.Sound(sound_path) |
||||
|
||||
# Get the appropriate channel |
||||
channel = self.audio_channels.get(tag, self.audio_channels["base"]) |
||||
|
||||
# Stop any currently playing sound on this channel |
||||
channel.stop() |
||||
|
||||
# Play the new sound |
||||
channel.play(sound) |
||||
|
||||
# Store reference to prevent garbage collection |
||||
self.current_sounds[tag] = sound |
||||
except Exception as e: |
||||
print(f"Error playing sound {sound_file}: {e}") |
||||
|
||||
def stop_sound(self): |
||||
"""Stop all audio playback""" |
||||
for channel in self.audio_channels.values(): |
||||
channel.stop() |
||||
|
||||
# ====================== |
||||
# INPUT METHODS |
||||
# ====================== |
||||
|
||||
def load_joystick(self): |
||||
"""Initialize joystick support""" |
||||
pygame.joystick.init() |
||||
joystick_count = pygame.joystick.get_count() |
||||
if joystick_count > 0: |
||||
self.joystick = pygame.joystick.Joystick(0) |
||||
self.joystick.init() |
||||
print(f"Joystick initialized: {self.joystick.get_name()}") |
||||
else: |
||||
self.joystick = None |
||||
|
||||
# ====================== |
||||
# MAIN GAME LOOP |
||||
# ====================== |
||||
|
||||
def _normalize_key_name(self, key): |
||||
"""Normalize pygame key names to match SDL2 key names""" |
||||
# Pygame returns lowercase, SDL2 returns with proper case |
||||
key_map = { |
||||
"return": "Return", |
||||
"escape": "Escape", |
||||
"space": "Space", |
||||
"tab": "Tab", |
||||
"left shift": "Left_Shift", |
||||
"right shift": "Right_Shift", |
||||
"left ctrl": "Left_Ctrl", |
||||
"right ctrl": "Right_Ctrl", |
||||
"left alt": "Left_Alt", |
||||
"right alt": "Right_Alt", |
||||
"up": "Up", |
||||
"down": "Down", |
||||
"left": "Left", |
||||
"right": "Right", |
||||
"delete": "Delete", |
||||
"backspace": "Backspace", |
||||
"insert": "Insert", |
||||
"home": "Home", |
||||
"end": "End", |
||||
"pageup": "Page_Up", |
||||
"pagedown": "Page_Down", |
||||
"f1": "F1", |
||||
"f2": "F2", |
||||
"f3": "F3", |
||||
"f4": "F4", |
||||
"f5": "F5", |
||||
"f6": "F6", |
||||
"f7": "F7", |
||||
"f8": "F8", |
||||
"f9": "F9", |
||||
"f10": "F10", |
||||
"f11": "F11", |
||||
"f12": "F12", |
||||
} |
||||
# Return mapped value or capitalize first letter of original |
||||
normalized = key_map.get(key.lower(), key) |
||||
# Handle single letters (make uppercase) |
||||
if len(normalized) == 1: |
||||
normalized = normalized.upper() |
||||
return normalized |
||||
|
||||
def mainloop(self, **kwargs): |
||||
"""Main game loop handling events and rendering""" |
||||
while self.running: |
||||
performance_start = pygame.time.get_ticks() |
||||
|
||||
# Clear screen |
||||
self.screen.fill((0, 0, 0)) |
||||
|
||||
# Execute background update if provided |
||||
if "bg_update" in kwargs: |
||||
kwargs["bg_update"]() |
||||
|
||||
# Execute main update |
||||
kwargs["update"]() |
||||
|
||||
# Update and draw white flash effect |
||||
if self.update_white_flash(): |
||||
self.draw_white_flash() |
||||
|
||||
# Handle Pygame events |
||||
for event in pygame.event.get(): |
||||
if event.type == pygame.QUIT: |
||||
self.running = False |
||||
elif event.type == pygame.KEYDOWN: |
||||
key = pygame.key.name(event.key) |
||||
key = self._normalize_key_name(key) |
||||
key = key.replace(" ", "_") |
||||
self.trigger(f"keydown_{key}") |
||||
elif event.type == pygame.KEYUP: |
||||
key = pygame.key.name(event.key) |
||||
key = self._normalize_key_name(key) |
||||
key = key.replace(" ", "_") |
||||
self.trigger(f"keyup_{key}") |
||||
elif event.type == pygame.MOUSEMOTION: |
||||
self.trigger(f"mousemove_{event.pos[0]}, {event.pos[1]}") |
||||
elif event.type == pygame.JOYBUTTONDOWN: |
||||
self.trigger(f"joybuttondown_{event.button}") |
||||
elif event.type == pygame.JOYBUTTONUP: |
||||
self.trigger(f"joybuttonup_{event.button}") |
||||
elif event.type == pygame.JOYHATMOTION: |
||||
self.trigger(f"joyhatmotion_{event.hat}_{event.value}") |
||||
|
||||
# Update display |
||||
pygame.display.flip() |
||||
|
||||
# Control frame rate |
||||
self.clock.tick(60) # Target 60 FPS |
||||
|
||||
# Calculate performance |
||||
self.performance = pygame.time.get_ticks() - performance_start |
||||
|
||||
def step(self, update=None, bg_update=None): |
||||
"""Execute a single frame iteration. This is non-blocking and useful when |
||||
the caller (JS) schedules frames via requestAnimationFrame in the browser. |
||||
""" |
||||
performance_start = pygame.time.get_ticks() |
||||
|
||||
# Clear screen |
||||
self.screen.fill((0, 0, 0)) |
||||
|
||||
# Background update |
||||
if bg_update: |
||||
try: |
||||
bg_update() |
||||
except Exception: |
||||
pass |
||||
|
||||
# Main update |
||||
if update: |
||||
try: |
||||
update() |
||||
except Exception: |
||||
pass |
||||
|
||||
# Update and draw white flash effect |
||||
if self.update_white_flash(): |
||||
self.draw_white_flash() |
||||
|
||||
# Handle Pygame events (single-frame processing) |
||||
for event in pygame.event.get(): |
||||
if event.type == pygame.QUIT: |
||||
self.running = False |
||||
elif event.type == pygame.KEYDOWN: |
||||
key = pygame.key.name(event.key) |
||||
key = self._normalize_key_name(key) |
||||
key = key.replace(" ", "_") |
||||
self.trigger(f"keydown_{key}") |
||||
elif event.type == pygame.KEYUP: |
||||
key = pygame.key.name(event.key) |
||||
key = self._normalize_key_name(key) |
||||
key = key.replace(" ", "_") |
||||
self.trigger(f"keyup_{key}") |
||||
elif event.type == pygame.MOUSEMOTION: |
||||
self.trigger(f"mousemove_{event.pos[0]}, {event.pos[1]}") |
||||
elif event.type == pygame.JOYBUTTONDOWN: |
||||
self.trigger(f"joybuttondown_{event.button}") |
||||
elif event.type == pygame.JOYBUTTONUP: |
||||
self.trigger(f"joybuttonup_{event.button}") |
||||
elif event.type == pygame.JOYHATMOTION: |
||||
self.trigger(f"joyhatmotion_{event.hat}_{event.value}") |
||||
|
||||
# Update display once per frame |
||||
pygame.display.flip() |
||||
|
||||
# Control frame rate |
||||
self.clock.tick(60) |
||||
|
||||
# Calculate performance |
||||
self.performance = pygame.time.get_ticks() - performance_start |
||||
|
||||
# ====================== |
||||
# SPECIAL EFFECTS |
||||
# ====================== |
||||
|
||||
def trigger_white_flash(self): |
||||
"""Trigger the white flash effect""" |
||||
self.white_flash_active = True |
||||
self.white_flash_start_time = pygame.time.get_ticks() |
||||
self.white_flash_opacity = 255 |
||||
|
||||
def update_white_flash(self): |
||||
"""Update the white flash effect and return True if it should be drawn""" |
||||
if not self.white_flash_active: |
||||
return False |
||||
|
||||
current_time = pygame.time.get_ticks() |
||||
elapsed_time = current_time - self.white_flash_start_time |
||||
|
||||
if elapsed_time < 500: # First 500ms: full white |
||||
self.white_flash_opacity = 255 |
||||
return True |
||||
elif elapsed_time < 2000: # Next 1500ms: fade out |
||||
fade_progress = (elapsed_time - 500) / 1500.0 |
||||
self.white_flash_opacity = int(255 * (1.0 - fade_progress)) |
||||
return True |
||||
else: # Effect is complete |
||||
self.white_flash_active = False |
||||
self.white_flash_opacity = 0 |
||||
return False |
||||
|
||||
def draw_white_flash(self): |
||||
"""Draw the white flash overlay""" |
||||
if self.white_flash_opacity > 0: |
||||
white_surface = pygame.Surface(self.target_size) |
||||
white_surface.fill((255, 255, 255)) |
||||
white_surface.set_alpha(self.white_flash_opacity) |
||||
self.screen.blit(white_surface, (0, 0)) |
||||
|
||||
# ====================== |
||||
# UTILITY METHODS |
||||
# ====================== |
||||
|
||||
def new_cycle(self, delay, callback): |
||||
"""Placeholder for cycle management (not needed in pygame implementation)""" |
||||
pass |
||||
|
||||
def full_screen(self, flag): |
||||
"""Toggle fullscreen mode""" |
||||
if flag: |
||||
self.window = pygame.display.set_mode(self.target_size, pygame.FULLSCREEN) |
||||
else: |
||||
self.window = pygame.display.set_mode(self.target_size) |
||||
self.screen = self.window |
||||
|
||||
def get_perf_counter(self): |
||||
"""Get performance counter for timing""" |
||||
return pygame.time.get_ticks() |
||||
|
||||
def close(self): |
||||
"""Close the game window and cleanup""" |
||||
self.running = False |
||||
pygame.quit() |
||||
|
||||
# ====================== |
||||
# BLOOD EFFECT METHODS |
||||
# ====================== |
||||
|
||||
def generate_blood_surface(self): |
||||
"""Generate a dynamic blood splatter surface using Pygame""" |
||||
size = self.cell_size |
||||
|
||||
# Create RGBA surface for blood splatter |
||||
blood_surface = pygame.Surface((size, size), pygame.SRCALPHA) |
||||
|
||||
# Blood color variations |
||||
blood_colors = [ |
||||
(139, 0, 0, 255), # Dark red |
||||
(34, 34, 34, 255), # Very dark gray |
||||
(20, 60, 60, 255), # Dark teal |
||||
(255, 0, 0, 255), # Pure red |
||||
(128, 0, 0, 255), # Reddish brown |
||||
] |
||||
|
||||
# Generate splatter with diffusion algorithm |
||||
center_x, center_y = size // 2, size // 2 |
||||
max_radius = size // 3 + random.randint(-3, 5) |
||||
|
||||
for y in range(size): |
||||
for x in range(size): |
||||
# Calculate distance from center |
||||
distance = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5 |
||||
|
||||
# Calculate blood probability based on distance |
||||
if distance <= max_radius: |
||||
probability = max(0, 1 - (distance / max_radius)) |
||||
noise = random.random() * 0.7 |
||||
|
||||
if random.random() < probability * noise: |
||||
color = random.choice(blood_colors) |
||||
alpha = int(255 * probability * random.uniform(0.6, 1.0)) |
||||
blood_surface.set_at((x, y), (*color[:3], alpha)) |
||||
|
||||
# Add scattered droplets around main splatter |
||||
for _ in range(random.randint(3, 8)): |
||||
drop_x = center_x + random.randint(-max_radius - 5, max_radius + 5) |
||||
drop_y = center_y + random.randint(-max_radius - 5, max_radius + 5) |
||||
|
||||
if 0 <= drop_x < size and 0 <= drop_y < size: |
||||
drop_size = random.randint(1, 3) |
||||
for dy in range(-drop_size, drop_size + 1): |
||||
for dx in range(-drop_size, drop_size + 1): |
||||
nx, ny = drop_x + dx, drop_y + dy |
||||
if 0 <= nx < size and 0 <= ny < size: |
||||
if random.random() < 0.6: |
||||
color = random.choice(blood_colors[:3]) |
||||
alpha = random.randint(100, 200) |
||||
blood_surface.set_at((nx, ny), (*color[:3], alpha)) |
||||
|
||||
return blood_surface |
||||
|
||||
def draw_blood_surface(self, blood_surface, position): |
||||
"""Convert blood surface to texture and return it""" |
||||
# In pygame, we can return the surface directly |
||||
return blood_surface |
||||
|
||||
def combine_blood_surfaces(self, existing_surface, new_surface): |
||||
"""Combine two blood surfaces by blending them together""" |
||||
combined_surface = pygame.Surface((self.cell_size, self.cell_size), pygame.SRCALPHA) |
||||
|
||||
# Blit existing blood first |
||||
combined_surface.blit(existing_surface, (0, 0)) |
||||
|
||||
# Blit new blood on top with alpha blending |
||||
combined_surface.blit(new_surface, (0, 0)) |
||||
|
||||
return combined_surface |
||||
|
||||
def free_surface(self, surface): |
||||
"""Safely free a pygame surface (not needed in pygame, handled by GC)""" |
||||
pass |
||||
|
||||
|
||||
class SpriteWrapper: |
||||
""" |
||||
Wrapper class to make pygame surfaces compatible with SDL2 sprite interface |
||||
""" |
||||
def __init__(self, surface): |
||||
self.surface = surface |
||||
self.size = surface.get_size() |
||||
self.position = (0, 0) |
||||
|
||||
def get_size(self): |
||||
return self.size |
||||
@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python3 |
||||
|
||||
import sys |
||||
import os |
||||
import sdl2 |
||||
import sdl2.ext |
||||
|
||||
class KeyLogger: |
||||
def __init__(self): |
||||
# Initialize SDL2 |
||||
sdl2.ext.init(joystick=True, video=True, audio=False) |
||||
# Initialize joystick support |
||||
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK) |
||||
sdl2.SDL_JoystickOpen(0) |
||||
sdl2.SDL_JoystickOpen(1) # Open the first joystick |
||||
sdl2.SDL_JoystickEventState(sdl2.SDL_ENABLE) |
||||
self.window = sdl2.ext.Window("Key Logger", size=(640, 480)) |
||||
self.window.show() |
||||
self.running = True |
||||
self.key_down = True |
||||
self.font = sdl2.ext.FontManager("assets/decterm.ttf", size=24) |
||||
|
||||
def run(self): |
||||
# Main loop |
||||
|
||||
while self.running: |
||||
# Handle SDL events |
||||
events = sdl2.ext.get_events() |
||||
for event in events: |
||||
self.event = event.type |
||||
if event.type == sdl2.SDL_KEYDOWN: |
||||
keycode = event.key.keysym.sym |
||||
# Log keycode to file |
||||
self.message = f"Key pressed: {sdl2.SDL_GetKeyName(keycode).decode('utf-8')}" |
||||
elif event.type == sdl2.SDL_KEYUP: |
||||
keycode = event.key.keysym.sym |
||||
# Log keycode to file |
||||
self.message = f"Key released: {sdl2.SDL_GetKeyName(keycode).decode('utf-8')}" |
||||
elif event.type == sdl2.SDL_JOYBUTTONDOWN: |
||||
button = event.jbutton.button |
||||
self.message = f"Joystick button {button} pressed" |
||||
if button == 9: # Assuming button 0 is the right trigger |
||||
self.running = False |
||||
elif event.type == sdl2.SDL_JOYBUTTONUP: |
||||
button = event.jbutton.button |
||||
self.message = f"Joystick button {button} released" |
||||
elif event.type == sdl2.SDL_JOYAXISMOTION: |
||||
axis = event.jaxis.axis |
||||
value = event.jaxis.value |
||||
self.message = f"Joystick axis {axis} moved to {value}" |
||||
elif event.type == sdl2.SDL_JOYHATMOTION: |
||||
hat = event.jhat.hat |
||||
value = event.jhat.value |
||||
self.message = f"Joystick hat {hat} moved to {value}" |
||||
elif event.type == sdl2.SDL_QUIT: |
||||
self.running = False |
||||
|
||||
# Update the window |
||||
sdl2.ext.fill(self.window.get_surface(), sdl2.ext.Color(34, 0, 33)) |
||||
greeting = self.font.render("Press any key...", color=sdl2.ext.Color(255, 255, 255)) |
||||
sdl2.SDL_BlitSurface(greeting, None, self.window.get_surface(), None) |
||||
if hasattr(self, 'message'): |
||||
text_surface = self.font.render(self.message, color=sdl2.ext.Color(255, 255, 255)) |
||||
sdl2.SDL_BlitSurface(text_surface, None, self.window.get_surface(), sdl2.SDL_Rect(0, 30, 640, 480)) |
||||
if hasattr(self, 'event'): |
||||
event_surface = self.font.render(f"Event: {self.event}", color=sdl2.ext.Color(255, 255, 255)) |
||||
sdl2.SDL_BlitSurface(event_surface, None, self.window.get_surface(), sdl2.SDL_Rect(0, 60, 640, 480)) |
||||
sdl2.SDL_UpdateWindowSurface(self.window.window) |
||||
# Refresh the window |
||||
|
||||
self.window.refresh() |
||||
sdl2.SDL_Delay(10) |
||||
# Check for quit event |
||||
if not self.running: |
||||
break |
||||
# Cleanup |
||||
sdl2.ext.quit() |
||||
|
||||
if __name__ == "__main__": |
||||
logger = KeyLogger() |
||||
logger.run() |
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue