Save work: Update game engine, maps, and add vision/pathing utilities. (Tests failing: vision raycast)
This commit is contained in:
+38
-12
@@ -204,27 +204,53 @@ class LLMClient:
|
||||
|
||||
## Configuration
|
||||
|
||||
### `config/game_settings.yaml`
|
||||
```yaml
|
||||
num_impostors: 2
|
||||
kill_cooldown: 25.0
|
||||
vision_range: 10.0
|
||||
impostor_vision_multiplier: 1.5
|
||||
light_sabotage_vision_multiplier: 0.25
|
||||
emergencies_per_player: 1
|
||||
confirm_ejects: true
|
||||
Use the CLI config manager for easy configuration:
|
||||
|
||||
```bash
|
||||
python scripts/config_manager.py settings list # List all settings
|
||||
python scripts/config_manager.py settings get kill_cooldown
|
||||
python scripts/config_manager.py settings set kill_cooldown 30
|
||||
python scripts/config_manager.py validate # Validate all configs
|
||||
```
|
||||
|
||||
### `config/game_settings.json`
|
||||
```json
|
||||
{
|
||||
"map_name": "skeld",
|
||||
"num_impostors": 2,
|
||||
"player_speed": 100.0,
|
||||
"crewmate_vision": 1.0,
|
||||
"impostor_vision": 1.5,
|
||||
"kill_cooldown": 25.0,
|
||||
"kill_distance": "medium",
|
||||
"emergencies_per_player": 1,
|
||||
"discussion_time": 30.0,
|
||||
"voting_time": 120.0,
|
||||
"confirm_ejects": true,
|
||||
"visual_tasks": true,
|
||||
"taskbar_updates": "always"
|
||||
}
|
||||
```
|
||||
|
||||
All measurements are in **pixels** (map is 2000x1500). Speed is pixels/second.
|
||||
|
||||
### `data/maps/skeld.json`
|
||||
```json
|
||||
{
|
||||
"canvas": {"width": 2000, "height": 1500},
|
||||
"rooms": [
|
||||
{"id": "cafeteria", "name": "Cafeteria", "tasks": [...], "vent": null},
|
||||
{"id": "cafeteria", "name": "Cafeteria", "center": [1000, 350], "bounds": [[850, 200], [1150, 500]]},
|
||||
...
|
||||
],
|
||||
"edges": [
|
||||
{"id": "cafe_weapons", "room_a": "cafeteria", "room_b": "weapons", "distance": 5.0},
|
||||
{"id": "cafe_weapons", "room_a": "cafeteria", "room_b": "weapons", "waypoints": [[1150, 350], [1300, 350]]},
|
||||
...
|
||||
]
|
||||
],
|
||||
"walls": [
|
||||
{"start": [850, 200], "end": [1150, 200]},
|
||||
...
|
||||
],
|
||||
"spawn_points": {"cafeteria": [1000, 400]}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# Ray-Traced FOV & Video Rendering Pipeline
|
||||
|
||||
## Current State
|
||||
- Vision is **room-based** (graph traversal)
|
||||
- Player sees everyone in same room
|
||||
- No pixel-level visibility, no wall occlusion
|
||||
|
||||
## Target State
|
||||
- **Ray-traced FOV** matching real Among Us
|
||||
- Walls block visibility
|
||||
- Circular vision radius from player
|
||||
- Light sabotage reduces radius
|
||||
- Video output at **60fps** for YouTube
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Ray-Traced FOV
|
||||
|
||||
### Map Data
|
||||
Current `skeld.json` has rooms + edges (corridors). Need to add:
|
||||
|
||||
```json
|
||||
{
|
||||
"walls": [
|
||||
{"p1": [100, 50], "p2": [100, 200]},
|
||||
{"p1": [100, 200], "p2": [250, 200]}
|
||||
],
|
||||
"spawn_positions": {"cafeteria": [300, 400]}
|
||||
}
|
||||
```
|
||||
|
||||
### Visibility Algorithm
|
||||
1. **Cast rays** from player position in 360° (e.g., 360 rays)
|
||||
2. **Intersect** each ray with wall segments
|
||||
3. **Closest intersection** per ray = vision boundary
|
||||
4. **Vision radius** clamps max distance
|
||||
5. **Player visible** if: within radius AND not occluded by walls
|
||||
|
||||
### Implementation
|
||||
- `src/engine/vision_raycast.py` — new module
|
||||
- `RaycastVisionSystem.get_visible_players(observer_pos, all_players, walls)`
|
||||
- Returns: list of visible player IDs + positions
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Engine Changes
|
||||
|
||||
### Current Position Model (`types.py`)
|
||||
```python
|
||||
@dataclass
|
||||
class Position:
|
||||
room_id: Optional[str] = None # Discrete room
|
||||
edge_id: Optional[str] = None # Walking between rooms
|
||||
progress: float = 0.0 # 0.0-1.0 on edge
|
||||
```
|
||||
|
||||
### New Position Model
|
||||
```python
|
||||
@dataclass
|
||||
class Position:
|
||||
x: float = 0.0 # Pixel X
|
||||
y: float = 0.0 # Pixel Y
|
||||
room_id: Optional[str] = None # Derived from position
|
||||
|
||||
def distance_to(self, other: "Position") -> float:
|
||||
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
|
||||
```
|
||||
|
||||
### New Map Data (`skeld.json`)
|
||||
```json
|
||||
{
|
||||
"rooms": [...],
|
||||
"edges": [...],
|
||||
"walls": [
|
||||
{"p1": [100, 50], "p2": [100, 200]},
|
||||
...
|
||||
],
|
||||
"room_polygons": {
|
||||
"cafeteria": [[x1,y1], [x2,y2], ...],
|
||||
...
|
||||
},
|
||||
"spawn_points": {
|
||||
"cafeteria": [300, 400],
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### New Module: `vision_raycast.py`
|
||||
```python
|
||||
class RaycastVision:
|
||||
def __init__(self, walls: list[Wall], vision_radius: float):
|
||||
...
|
||||
|
||||
def is_visible(self, from_pos: Position, to_pos: Position) -> bool:
|
||||
"""True if line-of-sight exists (no wall occlusion)."""
|
||||
...
|
||||
|
||||
def get_visible_players(self, observer: Position,
|
||||
all_players: list[Player]) -> list[Player]:
|
||||
"""Returns players within vision radius AND line-of-sight."""
|
||||
...
|
||||
|
||||
def get_vision_polygon(self, observer: Position) -> list[tuple]:
|
||||
"""For rendering: polygon representing visible area."""
|
||||
...
|
||||
```
|
||||
|
||||
### Wall Intersection Algorithm
|
||||
```python
|
||||
def ray_intersects_wall(ray_start, ray_dir, wall_p1, wall_p2) -> float | None:
|
||||
"""Returns distance to intersection, or None if no hit."""
|
||||
# Standard line-segment intersection math
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Rendering Pipeline
|
||||
|
||||
### Frame Generation (60fps)
|
||||
```
|
||||
replay.json → Renderer → frames/0001.png, 0002.png, ...
|
||||
```
|
||||
|
||||
### Per Frame:
|
||||
1. Draw map background (Skeld PNG)
|
||||
2. Apply FOV mask (ray-traced vignette)
|
||||
3. Draw player sprites at interpolated positions
|
||||
4. Draw bodies
|
||||
5. Overlay effects (kill, vent, sabotage)
|
||||
|
||||
### Video Assembly
|
||||
```bash
|
||||
ffmpeg -framerate 60 -i frames/%04d.png -c:v libx264 output.mp4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Assets
|
||||
|
||||
| Asset | Format | Source |
|
||||
|-------|--------|--------|
|
||||
| Skeld map | PNG | Fan art / game extract |
|
||||
| Crewmate sprites | Spritesheet | Available online |
|
||||
| Kill animations | Sprite sequence | Extract or recreate |
|
||||
| Meeting UI | HTML/PNG | Recreate |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Map upgrade** — Add walls + pixel coords
|
||||
2. **Raycast vision** — `vision_raycast.py`
|
||||
3. **Pixel positions** — Upgrade engine to (x,y)
|
||||
4. **Path interpolation** — Smooth walking
|
||||
5. **Frame renderer** — Pillow/Pygame
|
||||
6. **Meeting renderer** — Overlay
|
||||
7. **FFmpeg integration** — Stitch to video
|
||||
|
||||
## Questions
|
||||
|
||||
1. **POV style**: Single player POV, or omniscient?
|
||||
2. **Internal thoughts**: Show as subtitles?
|
||||
3. **TTS**: Voice for dialogue?
|
||||
Reference in New Issue
Block a user