fix alot of shit for webrtc to work

This commit is contained in:
2026-01-17 10:19:51 +01:00
parent f71b510cfc
commit eb718fa990
68 changed files with 6616 additions and 917 deletions

142
INSTALL_MATCHBOX.md Normal file
View File

@@ -0,0 +1,142 @@
# Installing Matchbox Signaling Server
## Step 1: Install Rust (if not already installed)
```bash
# Install Rust using rustup (recommended)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Follow the prompts, then restart your shell or run:
source "$HOME/.cargo/env"
# Verify installation
cargo --version
```
## Step 2: Install Matchbox Server
```bash
# Install matchbox_server
cargo install matchbox_server
# This will take a few minutes to compile
```
## Step 3: Run Matchbox Server
```bash
# Run on default port (3536)
matchbox_server
# Or specify a port
matchbox_server --port 3536
# Run in background with nohup
nohup matchbox_server --port 3536 > matchbox.log 2>&1 &
# Or with systemd (recommended for production)
# See systemd service example below
```
## Step 4: Configure Godot to Use Matchbox
Update `network_manager.gd` to connect to your matchbox server.
## Alternative: Use Pre-built Binary
If you don't want to install Rust, download a pre-built binary:
```bash
# Download latest release
wget https://github.com/johanhelsing/matchbox/releases/download/v0.9.0/matchbox_server-x86_64-unknown-linux-gnu.tar.gz
# Extract
tar -xzf matchbox_server-x86_64-unknown-linux-gnu.tar.gz
# Make executable
chmod +x matchbox_server
# Run it
./matchbox_server --port 3536
```
## Systemd Service (Production)
Create `/etc/systemd/system/matchbox.service`:
```ini
[Unit]
Description=Matchbox WebRTC Signaling Server
After=network.target
[Service]
Type=simple
User=oldcan
WorkingDirectory=/home/oldcan
ExecStart=/home/oldcan/.cargo/bin/matchbox_server --port 3536
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
Then:
```bash
# Reload systemd
sudo systemctl daemon-reload
# Start service
sudo systemctl start matchbox
# Enable on boot
sudo systemctl enable matchbox
# Check status
sudo systemctl status matchbox
# View logs
sudo journalctl -u matchbox -f
```
## Testing
Once running, test the connection:
```bash
# Check if it's listening
netstat -tulpn | grep 3536
# Or
ss -tulpn | grep 3536
```
Your matchbox server will be available at:
- `ws://ruinborn.thefirstboss.com:3536`
## Firewall Configuration
Make sure port 3536 is open:
```bash
# UFW
sudo ufw allow 3536/tcp
# Or iptables
sudo iptables -A INPUT -p tcp --dport 3536 -j ACCEPT
```
## Quick Start Commands
```bash
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# Install Matchbox
cargo install matchbox_server
# Run Matchbox
matchbox_server --port 3536
```

172
MULTIPLAYER_WEBRTC_GUIDE.md Normal file
View File

@@ -0,0 +1,172 @@
# WebRTC Multiplayer Guide
## How WebRTC Multiplayer Works
### Architecture
Your game now supports two networking modes:
1. **ENet (Native builds)**: Windows/Linux/Mac players connect via IP:Port
2. **WebRTC (Web builds)**: Browser players connect via Room IDs
**Important**: ENet and WebRTC are NOT compatible with each other!
- PC (ENet) players can only play with other PC (ENet) players
- Browser (WebRTC) players can only play with other Browser (WebRTC) players
### Your Server Setup
- **Matchbox Signaling**: `ws://ruinborn.thefirstboss.com:3536`
- **STUN Server**: `stun:ruinborn.thefirstboss.com:3578`
## How Players Find Each Other
### WebRTC/Browser Mode (Room-Based)
1. **Host creates a game**:
- Clicks "Host Game"
- Game generates a 6-character **Room Code** (e.g., "ABC123")
- Share this code with friends
2. **Players join**:
- Enter the **Room Code** in the "Join Game" field
- Click "Join"
- Matchbox connects all players in the same room
### Example Flow
```
Host (Browser):
- Clicks "Host"
- Gets Room Code: "XYZ789"
- Tells friend: "Join room XYZ789"
Player (Browser):
- Types "XYZ789" in join field
- Clicks "Join"
- Connected!
```
## Testing Scenarios
### Scenario 1: Browser to Browser (WebRTC)
**Works!**
```
1. Open game in Chrome: host.com/game
2. Host creates game → Gets code "ABC123"
3. Open game in Firefox: host.com/game
4. Join with code "ABC123"
5. Both connected via WebRTC!
```
### Scenario 2: PC to Browser (Mixed)
**Doesn't Work!**
```
PC (ENet) ←→ Browser (WebRTC) = INCOMPATIBLE
```
### Scenario 3: Phone Browser to PC Browser (WebRTC)
**Works!**
```
1. PC Browser hosts → code "XYZ456"
2. Phone browser joins "XYZ456"
3. Connected via WebRTC!
```
### Scenario 4: PC to PC (ENet)
**Works!**
```
1. PC1 hosts → Shows IP: 192.168.1.100
2. PC2 joins → Enters "192.168.1.100"
3. Connected via ENet!
```
## Can PC Host and Browser Join?
**Short Answer**: No, not with the current setup.
**Long Answer**: ENet (PC) and WebRTC (Browser) use completely different protocols. They can't talk to each other directly.
**Solution**: If you want cross-platform play, you have two options:
### Option A: Make PC Use WebRTC Too
Update PC builds to also use WebRTC when you want to play with browser users:
```gdscript
# Force WebRTC mode even on native builds
use_webrtc = true # in network_manager.gd
```
Then PC and Browser can use the same room codes!
### Option B: Use a Dedicated Server
Run a dedicated server that:
- Accepts both ENet and WebRTC connections
- Bridges between them
This is more complex but allows true cross-platform play.
## Room Codes
Room codes are automatically generated as 6-character codes:
- Easy to share (just text it!)
- No IP addresses to remember
- Works through NATs/firewalls
Example codes: `ABC123`, `XYZ789`, `PLAYER`, `GAME42`
## Exporting for Web
1. **Export HTML5**:
- Project → Export → Add → HTML5 (WebAssembly)
- Export the project
2. **Host on Web Server**:
```bash
# Your files
python -m http.server 8000
```
3. **Players Access**:
- `http://yoursite.com/game`
- Works on PC browsers, phone browsers, tablets!
## Security Notes
- Room codes are NOT secure (anyone with code can join)
- For production, add:
- Password-protected rooms
- Room capacity limits
- Ban/kick functionality
- Room expiration
## Troubleshooting
**"Connection failed" in browser:**
- Check matchbox server is running: `netstat -tulpn | grep 3536`
- Check firewall allows port 3536
- Check browser console for errors
**"Can't see other players":**
- Make sure both players entered the SAME room code
- Check both are using browser builds (not mixing PC/browser)
**"Stuck on connecting":**
- STUN server must be reachable
- Check port 3578 is open
- Try different browsers
## Current Limitations
1. PC (ENet) ←/→ Browser (WebRTC) can't play together
2. Need to share room codes manually (no lobby browser yet)
3. No reconnection if disconnected (must rejoin)
## Next Steps
To improve multiplayer:
1. Add lobby browser (see active rooms)
2. Add friend codes/invites
3. Add voice chat (WebRTC supports it!)
4. Add dedicated server for PC↔Browser bridge

166
NETWORK_MODE_GUIDE.md Normal file
View File

@@ -0,0 +1,166 @@
# Network Mode Selection Guide
## New Features
### 1. **Network Mode Dropdown** (PC Only)
On PC builds, you'll now see a dropdown in the main menu:
- **ENet (PC/LAN)**: Traditional IP-based networking for PC players
- **WebRTC (Browser/Web)**: Web-compatible networking using room codes
The dropdown is automatically hidden on web builds (WebRTC is auto-selected).
### 2. **--webrtc Command Line Flag**
Force WebRTC mode on PC builds:
```bash
# Start as host with WebRTC
godot --path . -- --host --webrtc
# Join with WebRTC
godot --path . -- --join --webrtc --address=ABC123
# Example with room code
godot --path . -- --join --webrtc --address=XYZ789
```
## Usage Examples
### PC to PC (Same LAN) - ENet Mode
**PC 1 (Host):**
1. Select "ENet (PC/LAN)" in dropdown
2. Click "Host Game"
3. Note your IP (e.g., 192.168.1.100)
**PC 2 (Join):**
1. Select "ENet (PC/LAN)" in dropdown
2. Enter host IP in "Server Address"
3. Click "Join Game"
### PC to PC (Internet) - WebRTC Mode
**PC 1 (Host):**
1. Select "WebRTC (Browser/Web)" in dropdown
2. Click "Host Game"
3. Share the 6-character room code (e.g., "ABC123")
**PC 2 (Join):**
1. Select "WebRTC (Browser/Web)" in dropdown
2. Enter room code in "Server Address"
3. Click "Join Game"
### PC to Browser - WebRTC Mode
**PC (Host):**
1. Select "WebRTC (Browser/Web)" in dropdown
2. Click "Host Game"
3. Share room code
**Browser (Join):**
1. Enter room code (WebRTC auto-detected)
2. Click "Join Game"
### Browser to Browser - WebRTC Mode
**Browser 1 (Host):**
1. Click "Host Game"
2. Share room code
**Browser 2 (Join):**
1. Enter room code
2. Click "Join Game"
## Command Line Examples
### Test with WebRTC
```bash
# Terminal 1 - Host with WebRTC
godot --path . -- --host --webrtc
# Terminal 2 - Join with WebRTC (room code from Terminal 1)
godot --path . -- --join --webrtc --address=ABC123
```
### Test with ENet (Default)
```bash
# Terminal 1 - Host
godot --path . -- --host
# Terminal 2 - Join
godot --path . -- --join --address=127.0.0.1
```
### Test with Multiple Local Players
```bash
# Host with 2 local players using WebRTC
godot --path . -- --host --webrtc --players=2
# Join with 2 local players
godot --path . -- --join --webrtc --address=ABC123 --players=2
```
## When to Use Each Mode
### Use **ENet** When:
- Playing on same LAN (local network)
- Both players are on PC (Windows/Linux/Mac)
- You want lower latency
- You have direct network access
### Use **WebRTC** When:
- Playing over the internet (through NATs/firewalls)
- Playing with browser users
- Playing on mobile browsers
- You want easy room code sharing (no IP addresses)
## Technical Details
### ENet
- Protocol: UDP
- Connection: Direct IP:Port
- Latency: Lower (direct connection)
- NAT Traversal: Requires port forwarding
- Compatibility: PC only
### WebRTC
- Protocol: UDP/DTLS via WebRTC
- Connection: Room codes via signaling server
- Latency: Slightly higher (STUN/relay)
- NAT Traversal: Automatic (STUN/TURN)
- Compatibility: PC, Browser, Mobile
### Your Servers
- **Matchbox Signaling**: `ws://ruinborn.thefirstboss.com:3536`
- **STUN Server**: `stun:ruinborn.thefirstboss.com:3578`
## Troubleshooting
**Dropdown not visible:**
- This is normal on web builds (WebRTC auto-selected)
**Can't connect in ENet mode:**
- Check firewall (port 21212)
- Verify IP address is correct
- Try WebRTC mode instead
**Can't connect in WebRTC mode:**
- Verify room code matches exactly
- Check matchbox server is running
- Try ENet mode for LAN play
**Room code not showing:**
- Only shown in console when hosting with WebRTC
- Will be displayed in UI in future updates
## Future Improvements
- [ ] Display room code in UI (not just console)
- [ ] Copy room code to clipboard button
- [ ] Lobby browser (see active rooms)
- [ ] Remember last used network mode
- [ ] Auto-detect best mode for connection

133
WEBRTC_SETUP.md Normal file
View File

@@ -0,0 +1,133 @@
# WebRTC Multiplayer Setup
The game now supports WebRTC multiplayer for WebAssembly (browser) builds!
## How It Works
The `NetworkManager` automatically detects the platform:
- **Native builds** (Windows, Linux, Mac): Uses **ENet** (traditional UDP networking)
- **Web builds**: Uses **WebRTC** (peer-to-peer browser networking)
## Important Note About WebRTC
WebRTC requires a **signaling mechanism** to establish connections between peers. This is different from ENet which uses direct IP:port connections.
### Current Implementation Status
**Done:**
- Auto-detection of web platform
- WebRTCMultiplayerPeer creation for host and client
- Basic connection setup
- **STUN server configured** (`stun:ruinborn.thefirstboss.com:3578`) for NAT traversal
⚠️ **Requires Additional Setup:**
- **Signaling server** to exchange connection info between peers
- Session Description Protocol (SDP) offer/answer exchange
- ICE candidate exchange
## Quick Setup Options
### Option 1: Use a Public Signaling Server (Easiest)
Use a service like:
- [peerjs-server](https://github.com/peers/peerjs-server)
- [Matchbox](https://github.com/johanhelsing/matchbox) (Godot-specific)
- Roll your own simple WebSocket server
### Option 2: Manual Connection (For Testing)
For local testing, you can manually exchange connection strings:
1. Host creates a game
2. Host copies their connection info
3. Joiner pastes host's connection info
4. Joiner generates their own connection info
5. Host pastes joiner's connection info
### Option 3: Implement Signaling Server (Recommended for Production)
Create a simple WebSocket signaling server that:
1. Maintains a list of active lobbies
2. Relays SDP offers/answers between peers
3. Relays ICE candidates between peers
## Example: Using Matchbox (Recommended)
Matchbox is a simple signaling server specifically for Godot WebRTC:
```bash
# Install matchbox server
cargo install matchbox_server
# Run the server
matchbox_server
```
Then update the code to connect to your matchbox server when in web mode.
## Testing WebRTC Locally
1. **Export for HTML5**:
- Project -> Export -> Add -> HTML5 (WebAssembly)
- Export the project
2. **Run a local web server**:
```bash
# Python 3
python -m http.server 8000
# Or use any web server
```
3. **Open in browsers**:
- Host: `http://localhost:8000`
- Client: `http://localhost:8000` (in another browser tab/window)
4. **Connect**:
- One browser hosts
- Other browser joins
- (Requires signaling server to be running)
## Alternative: WebSocket Multiplayer
If WebRTC setup is too complex, you can use WebSocketMultiplayerPeer instead:
```gdscript
# In network_manager.gd, replace WebRTCMultiplayerPeer with:
var peer = WebSocketMultiplayerPeer.new()
# For host:
peer.create_server(port)
# For client:
peer.create_client("ws://your-server:port")
```
WebSocket is simpler but requires a dedicated server (can't be P2P like WebRTC).
## Production Deployment
For production, you'll need:
1. A signaling server (for WebRTC) or game server (for WebSocket)
2. HTTPS hosting (required for WebRTC in browsers)
3. Proper CORS configuration
4. STUN/TURN servers for NAT traversal (optional but recommended)
## Current Status
The game will:
- ✅ Detect web platform automatically
- ✅ Create WebRTC peers
- ⚠️ Require signaling server setup to actually connect
**To complete the setup**, implement one of the options above based on your needs.
## Quick Recommendation
**For Testing**: Use Matchbox (Option 3)
**For Production**: Implement a proper signaling server with lobby/matchmaking
## Resources
- [Godot WebRTC Documentation](https://docs.godotengine.org/en/stable/tutorials/networking/webrtc.html)
- [Matchbox Server](https://github.com/johanhelsing/matchbox)
- [WebRTC for Godot Tutorial](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html#webrtc)

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://ba6csajuxujrg"
path="res://.godot/imported/Gelhein - Evil.mp3-bc2ead9945dee5ecfc0f8aff80053da8.mp3str"
[deps]
source_file="res://assets/audio/music/Gelhein - Evil.mp3"
dest_files=["res://.godot/imported/Gelhein - Evil.mp3-bc2ead9945dee5ecfc0f8aff80053da8.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -79,7 +79,7 @@ dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter="webrtc/webrtc.gdextension"
export_path="../export/www/index.html" export_path="../export/www/index.html"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false

View File

@@ -24,12 +24,14 @@ buses/default_bus_layout="uid://psistrevppd1"
[autoload] [autoload]
NetworkManager="*res://scripts/network_manager.gd" NetworkManager="*res://scripts/network_manager.gd"
LogManager="*res://scripts/log_manager.gd"
[display] [display]
window/size/viewport_width=1280 window/size/viewport_width=1280
window/size/viewport_height=720 window/size/viewport_height=720
window/stretch/mode="canvas_items" window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
window/stretch/scale_mode="integer" window/stretch/scale_mode="integer"
[editor_plugins] [editor_plugins]
@@ -65,6 +67,7 @@ move_down={
grab={ grab={
"deadzone": 0.5, "deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":74,"key_label":0,"unicode":106,"location":0,"echo":false,"script":null)
] ]
} }
throw={ throw={
@@ -75,6 +78,7 @@ throw={
attack={ attack={
"deadzone": 0.5, "deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":75,"key_label":0,"unicode":107,"location":0,"echo":false,"script":null)
] ]
} }

View File

@@ -158,6 +158,27 @@ horizontal_alignment = 1
layout_mode = 2 layout_mode = 2
texture_progress = ExtResource("4_hearts_filled") texture_progress = ExtResource("4_hearts_filled")
[node name="CenterTop" type="MarginContainer" parent="." unique_id=22752256]
anchors_preset = 7
anchor_left = 0.5
anchor_top = 0.0
anchor_right = 0.5
anchor_bottom = 0.0
offset_left = -150.0
offset_top = 8.0
offset_right = 150.0
offset_bottom = 28.0
grow_horizontal = 2
theme = SubResource("Theme_standard_font")
[node name="LabelDisconnected" type="Label" parent="CenterTop" unique_id=869912310]
layout_mode = 2
theme = SubResource("Theme_standard_font")
text = "Disconnected - Reconnecting..."
horizontal_alignment = 1
vertical_alignment = 1
visible = false
[node name="MobileInput" type="Control" parent="." unique_id=1373461519] [node name="MobileInput" type="Control" parent="." unique_id=1373461519]
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15

View File

@@ -103,6 +103,30 @@ layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
placeholder_text = "ruinborn.thefirstboss.com" placeholder_text = "ruinborn.thefirstboss.com"
[node name="RoomFetchStatusContainer" type="VBoxContainer" parent="Control/MainMenu/VBoxContainer" unique_id=1286608618]
visible = false
layout_mode = 2
[node name="RoomFetchLoadingContainer" type="HBoxContainer" parent="Control/MainMenu/VBoxContainer/RoomFetchStatusContainer" unique_id=1286608619]
layout_mode = 2
[node name="LoadingLabel" type="Label" parent="Control/MainMenu/VBoxContainer/RoomFetchStatusContainer/RoomFetchLoadingContainer" unique_id=1286608620]
layout_mode = 2
size_flags_horizontal = 3
text = "Loading rooms..."
vertical_alignment = 1
[node name="LoadingSpinner" type="Label" parent="Control/MainMenu/VBoxContainer/RoomFetchStatusContainer/RoomFetchLoadingContainer" unique_id=1286608621]
layout_mode = 2
text = "⏳"
horizontal_alignment = 1
[node name="LastFetchLabel" type="Label" parent="Control/MainMenu/VBoxContainer/RoomFetchStatusContainer" unique_id=1286608622]
layout_mode = 2
text = "Last fetched: Never"
horizontal_alignment = 1
autowrap_mode = 2
[node name="Spacer2" type="Control" parent="Control/MainMenu/VBoxContainer" unique_id=1061067008] [node name="Spacer2" type="Control" parent="Control/MainMenu/VBoxContainer" unique_id=1061067008]
custom_minimum_size = Vector2(0, 20) custom_minimum_size = Vector2(0, 20)
layout_mode = 2 layout_mode = 2

View File

@@ -156,7 +156,13 @@ func send_system_message(message: String):
if multiplayer.is_server(): if multiplayer.is_server():
# Server broadcasts to all clients and also shows locally # Server broadcasts to all clients and also shows locally
_add_message("System", message) # Show locally first _add_message("System", message) # Show locally first
_receive_message.rpc("System", message) # Broadcast to clients # Route through game_world to avoid node path issues
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_rpc_to_ready_peers"):
game_world._rpc_to_ready_peers("_receive_chat_message", ["System", message])
else:
# Fallback: try direct RPC if game_world not available
_receive_message.rpc("System", message)
else: else:
# Client sends to server (system messages should only come from server) # Client sends to server (system messages should only come from server)
pass pass
@@ -164,6 +170,16 @@ func send_system_message(message: String):
# Offline mode - just show locally # Offline mode - just show locally
_add_message("System", message) _add_message("System", message)
func add_local_message(player_name: String, message: String):
# Add a local-only message (not broadcast to other players)
# Used for connection status messages that should only appear locally
_add_message(player_name, message)
func add_colorful_local_message(player_name: String, message: String):
# Add a local-only message with each character in a different color
# Used for special connection status messages
_add_colorful_message(player_name, message)
func _send_message(message: String): func _send_message(message: String):
if not network_manager: if not network_manager:
return return
@@ -176,7 +192,13 @@ func _send_message(message: String):
if multiplayer.is_server(): if multiplayer.is_server():
# Server broadcasts to all clients and also shows locally # Server broadcasts to all clients and also shows locally
_add_message(player_name, message) # Show locally first _add_message(player_name, message) # Show locally first
_receive_message.rpc(player_name, message) # Broadcast to clients # Route through game_world to avoid node path issues
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_rpc_to_ready_peers"):
game_world._rpc_to_ready_peers("_receive_chat_message", [player_name, message])
else:
# Fallback: try direct RPC if game_world not available
_receive_message.rpc(player_name, message)
else: else:
# Client sends to server # Client sends to server
_send_message_to_server.rpc_id(1, player_name, message) _send_message_to_server.rpc_id(1, player_name, message)
@@ -190,7 +212,12 @@ func _send_message_to_server(player_name: String, message: String):
if multiplayer.is_server(): if multiplayer.is_server():
# Show message on server first # Show message on server first
_add_message(player_name, message) _add_message(player_name, message)
# Then broadcast to all clients # Then broadcast to all clients via game_world to avoid node path issues
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_rpc_to_ready_peers"):
game_world._rpc_to_ready_peers("_receive_chat_message", [player_name, message])
else:
# Fallback: try direct RPC if game_world not available
_receive_message.rpc(player_name, message) _receive_message.rpc(player_name, message)
@rpc("authority", "reliable") @rpc("authority", "reliable")
@@ -217,7 +244,7 @@ func _get_player_color(player_name: String) -> Color:
func _add_message(player_name: String, message: String): func _add_message(player_name: String, message: String):
if not message_list: if not message_list:
print("ChatUI: ERROR - message_list is null, cannot add message!") LogManager.log_error("ChatUI: ERROR - message_list is null, cannot add message!", LogManager.CATEGORY_UI)
return return
# Get current time # Get current time
@@ -271,6 +298,83 @@ func _add_message(player_name: String, message: String):
# Update background visibility # Update background visibility
_update_background_visibility() _update_background_visibility()
func _add_colorful_message(player_name: String, message: String):
# Add a message with each character in a different color
if not message_list:
LogManager.log_error("ChatUI: ERROR - message_list is null, cannot add message!", LogManager.CATEGORY_UI)
return
# Get current time
var time_dict = Time.get_time_dict_from_system()
var timestamp = "%02d:%02d:%02d" % [time_dict.hour, time_dict.minute, time_dict.second]
var current_time = Time.get_ticks_msec() / 1000.0
# Add to messages array
messages.append({
"timestamp": timestamp,
"player_name": player_name,
"message": message,
"time": current_time
})
# Limit message history
if messages.size() > MAX_MESSAGES:
messages.pop_front()
# Get player color for the player name
var player_color = _get_player_color(player_name)
var player_color_hex = "#%02x%02x%02x" % [int(player_color.r * 255), int(player_color.g * 255), int(player_color.b * 255)]
# Create message label using RichTextLabel for colored text
var message_label = RichTextLabel.new()
message_label.name = "Message_%d" % messages.size()
message_label.bbcode_enabled = true
message_label.scroll_active = false
message_label.fit_content = true
message_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
message_label.clip_contents = false
# Color palette for rainbow effect
var colors = [
Color.RED,
Color.ORANGE,
Color.YELLOW,
Color.GREEN,
Color.CYAN,
Color.BLUE,
Color(0.5, 0.0, 0.5), # Purple
Color.MAGENTA
]
# Format message with colored player name and colorful message text
var formatted_text = "%s [color=%s]%s[/color]: " % [timestamp, player_color_hex, player_name]
# Add each character with a different color
for i in range(message.length()):
var char_text = message.substr(i, 1)
var color = colors[i % colors.size()]
var color_hex = "#%02x%02x%02x" % [int(color.r * 255), int(color.g * 255), int(color.b * 255)]
formatted_text += "[color=%s]%s[/color]" % [color_hex, char_text]
message_label.text = formatted_text
# Apply Metropolis font if available
if metropolis_font:
message_label.add_theme_font_override("normal_font", metropolis_font)
message_label.add_theme_font_size_override("normal_font_size", 12)
message_label.modulate.a = 1.0
# Add to message list (at the end, so newest messages appear closest to input)
# Messages flow upwards - oldest at top, newest at bottom
message_list.add_child(message_label)
# Scroll to bottom
call_deferred("_scroll_to_bottom")
# Update background visibility
_update_background_visibility()
func _scroll_to_bottom(): func _scroll_to_bottom():
if message_scroll: if message_scroll:
await get_tree().process_frame # Wait for layout to update await get_tree().process_frame # Wait for layout to update
@@ -350,7 +454,17 @@ func _process(_delta):
_update_message_fades() _update_message_fades()
func _get_player_name() -> String: func _get_player_name() -> String:
# Get local player name # Get local player name from network_manager.players_info
# This ensures consistency with how player names are generated and used elsewhere
if network_manager and multiplayer.has_multiplayer_peer():
var my_id = multiplayer.get_unique_id()
var player_info = network_manager.get_player_info(my_id)
if not player_info.is_empty():
var player_names = player_info.get("player_names", [])
if player_names.size() > 0:
return player_names[0]
# Fallback: try to get from player node
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
if game_world: if game_world:
var player_manager = game_world.get_node_or_null("PlayerManager") var player_manager = game_world.get_node_or_null("PlayerManager")
@@ -361,7 +475,7 @@ func _get_player_name() -> String:
# peer_id and local_player_index are always defined in player.gd # peer_id and local_player_index are always defined in player.gd
return "Player%d_%d" % [player.peer_id, player.local_player_index + 1] return "Player%d_%d" % [player.peer_id, player.local_player_index + 1]
# Fallback # Final fallback
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
return "Player%d" % multiplayer.get_unique_id() return "Player%d" % multiplayer.get_unique_id()
return "Player" return "Player"

View File

@@ -102,6 +102,12 @@ func _ready() -> void:
# Call setup after a frame to ensure everything is ready # Call setup after a frame to ensure everything is ready
call_deferred("_ready_after_setup") call_deferred("_ready_after_setup")
# Ensure door_index meta is set if name follows BlockingDoor_<index>
if name.begins_with("BlockingDoor_") and not has_meta("door_index"):
var index_str = name.substr(13)
if index_str.is_valid_int():
set_meta("door_index", index_str.to_int())
func _update_door_texture(): func _update_door_texture():
# Update door texture based on door type # Update door texture based on door type
var sprite = get_node_or_null("Sprite2D") var sprite = get_node_or_null("Sprite2D")
@@ -113,24 +119,24 @@ func _update_door_texture():
var locked_texture = load("res://assets/gfx/door_locked.png") var locked_texture = load("res://assets/gfx/door_locked.png")
if locked_texture: if locked_texture:
sprite.texture = locked_texture sprite.texture = locked_texture
print("Door: Set KeyDoor texture to door_locked.png") LogManager.log("Door: Set KeyDoor texture to door_locked.png", LogManager.CATEGORY_DOOR)
else: else:
push_error("Door: Could not load door_locked.png texture!") LogManager.log_error("Door: Could not load door_locked.png texture!", LogManager.CATEGORY_DOOR)
"GateDoor": "GateDoor":
var gate_texture = load("res://assets/gfx/door_gate.png") var gate_texture = load("res://assets/gfx/door_gate.png")
if gate_texture: if gate_texture:
sprite.texture = gate_texture sprite.texture = gate_texture
print("Door: Set GateDoor texture to door_gate.png") LogManager.log("Door: Set GateDoor texture to door_gate.png", LogManager.CATEGORY_DOOR)
else: else:
push_error("Door: Could not load door_gate.png texture!") LogManager.log_error("Door: Could not load door_gate.png texture!", LogManager.CATEGORY_DOOR)
"StoneDoor": "StoneDoor":
# Use door_barred.png for stone doors # Use door_barred.png for stone doors
var barred_texture = load("res://assets/gfx/door_barred.png") var barred_texture = load("res://assets/gfx/door_barred.png")
if barred_texture: if barred_texture:
sprite.texture = barred_texture sprite.texture = barred_texture
print("Door: Set StoneDoor texture to door_barred.png") LogManager.log("Door: Set StoneDoor texture to door_barred.png", LogManager.CATEGORY_DOOR)
else: else:
push_error("Door: Could not load door_barred.png texture!") LogManager.log_error("Door: Could not load door_barred.png texture!", LogManager.CATEGORY_DOOR)
# Called every frame. 'delta' is the elapsed time since the previous frame. # Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void: func _process(delta: float) -> void:
@@ -138,7 +144,7 @@ func _process(delta: float) -> void:
if is_opening or is_closing: if is_opening or is_closing:
# Safety check: ensure closed_position is valid before animating # Safety check: ensure closed_position is valid before animating
if closed_position == Vector2.ZERO: if closed_position == Vector2.ZERO:
print("Door: ERROR - closed_position is zero during animation! Resetting...") LogManager.log_error("Door: ERROR - closed_position is zero during animation! Resetting...", LogManager.CATEGORY_DOOR)
closed_position = position - open_offset if is_opening else position closed_position = position - open_offset if is_opening else position
is_opening = false is_opening = false
is_closing = false is_closing = false
@@ -160,7 +166,7 @@ func _process(delta: float) -> void:
global_position = position # Also update global position during animation global_position = position # Also update global position during animation
# Debug: log for KeyDoors to verify movement # Debug: log for KeyDoors to verify movement
if type == "KeyDoor" and move_timer < 0.1: # Only log once at start of animation if type == "KeyDoor" and move_timer < 0.1: # Only log once at start of animation
print("Door: KeyDoor opening animation - start: ", start_pos, ", target: ", target_pos, ", offset: ", open_offset, ", direction: ", direction) LogManager.log("Door: KeyDoor opening animation - start: " + str(start_pos) + ", target: " + str(target_pos) + ", offset: " + str(open_offset) + ", direction: " + str(direction), LogManager.CATEGORY_DOOR)
# For KeyDoors: disable collision as soon as opening starts (allow passage immediately) # For KeyDoors: disable collision as soon as opening starts (allow passage immediately)
# For StoneDoor/GateDoor: update collision based on position # For StoneDoor/GateDoor: update collision based on position
@@ -211,7 +217,8 @@ func _process(delta: float) -> void:
global_position = open_position # Also set global position global_position = open_position # Also set global position
# When moved from closed position (open), collision should be DISABLED # When moved from closed position (open), collision should be DISABLED
set_collision_layer_value(7, false) set_collision_layer_value(7, false)
print("Door: Opening animation complete - moved to open position: ", open_position, " (closed: ", closed_position, ", offset: ", open_offset, ") - collision DISABLED", " (key_used=", key_used, ")" if type == "KeyDoor" else "") var key_used_str = " (key_used=" + str(key_used) + ")" if type == "KeyDoor" else ""
LogManager.log("Door: Opening animation complete - moved to open position: " + str(open_position) + " (closed: " + str(closed_position) + ", offset: " + str(open_offset) + ") - collision DISABLED" + key_used_str, LogManager.CATEGORY_DOOR)
# CRITICAL: For KeyDoors, ensure key_used is true after animation completes # CRITICAL: For KeyDoors, ensure key_used is true after animation completes
# This prevents the door from being reset to closed in _process() # This prevents the door from being reset to closed in _process()
@@ -234,7 +241,7 @@ func _process(delta: float) -> void:
global_position = closed_position # Also set global position global_position = closed_position # Also set global position
# When at closed position, collision should be ENABLED # When at closed position, collision should be ENABLED
set_collision_layer_value(7, true) set_collision_layer_value(7, true)
print("Door: Closing animation complete - moved to closed position: ", closed_position, " - collision ENABLED") LogManager.log("Door: Closing animation complete - moved to closed position: " + str(closed_position) + " - collision ENABLED", LogManager.CATEGORY_DOOR)
# Spawn smoke puffs when StoneDoor finishes closing (1-3 puffs) # Spawn smoke puffs when StoneDoor finishes closing (1-3 puffs)
if type == "StoneDoor": if type == "StoneDoor":
@@ -285,7 +292,7 @@ func _process(delta: float) -> void:
# Snap to closed position if somehow moved (shouldn't happen, but safety check) # Snap to closed position if somehow moved (shouldn't happen, but safety check)
var distance_to_closed = position.distance_to(closed_position) var distance_to_closed = position.distance_to(closed_position)
if distance_to_closed > 1.0: if distance_to_closed > 1.0:
print("Door: KeyDoor was moved incorrectly! Resetting to closed position.") LogManager.log("Door: KeyDoor was moved incorrectly! Resetting to closed position.", LogManager.CATEGORY_DOOR)
position = closed_position position = closed_position
is_closed = true is_closed = true
set_collision_layer_value(7, true) set_collision_layer_value(7, true)
@@ -329,9 +336,9 @@ func _open():
global_position = closed_position global_position = closed_position
is_closed = true is_closed = true
set_collision_layer_value(7, true) # Collision enabled at closed position set_collision_layer_value(7, true) # Collision enabled at closed position
print("Door: KeyDoor _open() called - reset to closed position ", closed_position, " before opening") LogManager.log("Door: KeyDoor _open() called - reset to closed position " + str(closed_position) + " before opening", LogManager.CATEGORY_DOOR)
else: else:
push_error("Door: KeyDoor _open() called but closed_position is zero!") LogManager.log_error("Door: KeyDoor _open() called but closed_position is zero!", LogManager.CATEGORY_DOOR)
return return
$SfxOpenKeyDoor.play() $SfxOpenKeyDoor.play()
else: else:
@@ -341,7 +348,7 @@ func _open():
if is_actually_open: if is_actually_open:
# Door is already open - don't do anything # Door is already open - don't do anything
print("Door: _open() called but door is already open! Position: ", position, ", closed: ", closed_position, ", distance: ", distance_to_closed) LogManager.log("Door: _open() called but door is already open! Position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(distance_to_closed), LogManager.CATEGORY_DOOR)
# Ensure door is at open position and collision is disabled # Ensure door is at open position and collision is disabled
var open_pos = closed_position + open_offset var open_pos = closed_position + open_offset
position = open_pos position = open_pos
@@ -356,9 +363,9 @@ func _open():
global_position = closed_position global_position = closed_position
is_closed = true is_closed = true
set_collision_layer_value(7, true) set_collision_layer_value(7, true)
print("Door: StoneDoor/GateDoor _open() called - ensuring door is at closed position ", closed_position, " before opening") LogManager.log("Door: StoneDoor/GateDoor _open() called - ensuring door is at closed position " + str(closed_position) + " before opening", LogManager.CATEGORY_DOOR)
else: else:
push_error("Door: StoneDoor/GateDoor _open() called but closed_position is zero!") LogManager.log_error("Door: StoneDoor/GateDoor _open() called but closed_position is zero!", LogManager.CATEGORY_DOOR)
return return
if type == "GateDoor": if type == "GateDoor":
$SfxOpenGateDoor.play() $SfxOpenGateDoor.play()
@@ -367,16 +374,29 @@ func _open():
# CRITICAL: Store starting position for animation (should be closed_position) # CRITICAL: Store starting position for animation (should be closed_position)
animation_start_position = position animation_start_position = position
print("Door: Starting open animation from ", animation_start_position, " to ", closed_position + open_offset, " (offset: ", open_offset, ")") LogManager.log("Door: Starting open animation from " + str(animation_start_position) + " to " + str(closed_position + open_offset) + " (offset: " + str(open_offset) + ")", LogManager.CATEGORY_DOOR)
is_opening = true is_opening = true
is_closing = false is_closing = false
move_timer = 0.0 move_timer = 0.0
# Sync door opening to clients in multiplayer # Sync door opening to clients in multiplayer
if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and is_inside_tree(): if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and is_inside_tree():
_sync_door_open.rpc() var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_rpc_to_ready_peers"):
game_world._rpc_to_ready_peers("_sync_door_open_by_name", [name])
# Also sync puzzle_solved state # Also sync puzzle_solved state
_sync_puzzle_solved.rpc(puzzle_solved) game_world._rpc_to_ready_peers("_sync_door_puzzle_solved_by_name", [name, puzzle_solved])
# Track door state for syncing to new clients
if game_world:
game_world.door_states[name] = {
"is_closed": false,
"puzzle_solved": puzzle_solved,
"key_used": key_used if "key_used" in self else false,
"position": position,
"closed_position": closed_position,
"open_offset": open_offset
}
func _close(): func _close():
# Only close on server/authority in multiplayer, then sync to clients # Only close on server/authority in multiplayer, then sync to clients
@@ -385,30 +405,30 @@ func _close():
# CRITICAL: KeyDoors should NEVER be closed (they only open with a key and stay open) # CRITICAL: KeyDoors should NEVER be closed (they only open with a key and stay open)
if type == "KeyDoor": if type == "KeyDoor":
print("Door: ERROR - _close() called on KeyDoor! KeyDoors should never be closed!") LogManager.log_error("Door: ERROR - _close() called on KeyDoor! KeyDoors should never be closed!", LogManager.CATEGORY_DOOR)
return return
# Ensure closed_position is valid before closing # Ensure closed_position is valid before closing
if closed_position == Vector2.ZERO: if closed_position == Vector2.ZERO:
# If closed_position wasn't set correctly, use current position # If closed_position wasn't set correctly, use current position
closed_position = position closed_position = position
print("Door: WARNING - closed_position was zero, using current position: ", closed_position) LogManager.log("Door: WARNING - closed_position was zero, using current position: " + str(closed_position), LogManager.CATEGORY_DOOR)
# Check both flag and actual position to determine door state # Check both flag and actual position to determine door state
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0 var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
var is_actually_at_closed = distance_to_closed < 5.0 # Within 5 pixels of closed position var is_actually_at_closed = distance_to_closed < 5.0 # Within 5 pixels of closed position
print("Door: _close() called - is_closed: ", is_closed, ", is_actually_at_closed: ", is_actually_at_closed, ", position: ", position, ", closed: ", closed_position, ", distance: ", distance_to_closed) LogManager.log("Door: _close() called - is_closed: " + str(is_closed) + ", is_actually_at_closed: " + str(is_actually_at_closed) + ", position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(distance_to_closed), LogManager.CATEGORY_DOOR)
# If door is already at closed position (both visually and by flag), don't do anything # If door is already at closed position (both visually and by flag), don't do anything
if is_closed and is_actually_at_closed and not is_opening and not is_closing: if is_closed and is_actually_at_closed and not is_opening and not is_closing:
print("Door: Already closed (both flag and position match), not closing again") LogManager.log("Door: Already closed (both flag and position match), not closing again", LogManager.CATEGORY_DOOR)
return # Already closed, don't do anything return # Already closed, don't do anything
# CRITICAL: If door is at closed position but flag says open, just fix the state - don't animate # CRITICAL: If door is at closed position but flag says open, just fix the state - don't animate
if is_actually_at_closed and not is_closed: if is_actually_at_closed and not is_closed:
# Door is visually at closed position but flag says open - fix state only # Door is visually at closed position but flag says open - fix state only
print("Door: Door is at closed position but flag says open! Fixing state only (no animation)") LogManager.log("Door: Door is at closed position but flag says open! Fixing state only (no animation)", LogManager.CATEGORY_DOOR)
position = closed_position # Ensure exact position position = closed_position # Ensure exact position
is_closed = true is_closed = true
set_collision_layer_value(7, true) set_collision_layer_value(7, true)
@@ -424,7 +444,7 @@ func _close():
# If door is significantly away from expected open position, snap to open position first # If door is significantly away from expected open position, snap to open position first
if distance_to_open > 10.0: if distance_to_open > 10.0:
# Door is very far from expected open position - reset to open position first # Door is very far from expected open position - reset to open position first
print("Door: WARNING - Door is far from expected open position! Resetting to open: ", expected_open_pos, " (was at: ", position, ", distance: ", distance_to_open, ")") LogManager.log("Door: WARNING - Door is far from expected open position! Resetting to open: " + str(expected_open_pos) + " (was at: " + str(position) + ", distance: " + str(distance_to_open) + ")", LogManager.CATEGORY_DOOR)
animation_start_position = expected_open_pos animation_start_position = expected_open_pos
position = expected_open_pos position = expected_open_pos
global_position = expected_open_pos global_position = expected_open_pos
@@ -434,7 +454,7 @@ func _close():
# Door is at or near open position - use current position as start # Door is at or near open position - use current position as start
animation_start_position = position animation_start_position = position
print("Door: Starting close animation from ", animation_start_position, " to ", closed_position, " (offset: ", open_offset, ")") LogManager.log("Door: Starting close animation from " + str(animation_start_position) + " to " + str(closed_position) + " (offset: " + str(open_offset) + ")", LogManager.CATEGORY_DOOR)
if type == "GateDoor": if type == "GateDoor":
$SfxCloseGateDoor.play() $SfxCloseGateDoor.play()
else: else:
@@ -446,7 +466,20 @@ func _close():
# Sync door closing to clients in multiplayer # Sync door closing to clients in multiplayer
if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and is_inside_tree(): if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and is_inside_tree():
_sync_door_close.rpc() var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_rpc_to_ready_peers"):
game_world._rpc_to_ready_peers("_sync_door_close_by_name", [name])
# Track door state for syncing to new clients
if game_world:
game_world.door_states[name] = {
"is_closed": true,
"puzzle_solved": puzzle_solved,
"key_used": key_used if "key_used" in self else false,
"position": position,
"closed_position": closed_position,
"open_offset": open_offset
}
func _ready_after_setup(): func _ready_after_setup():
# Called after door is fully set up with room references and positioned # Called after door is fully set up with room references and positioned
@@ -454,7 +487,7 @@ func _ready_after_setup():
# The position set by game_world is the OPEN position (initial state for blocking doors) # The position set by game_world is the OPEN position (initial state for blocking doors)
var open_position = position # Current position is the OPEN position (from tile coordinates) var open_position = position # Current position is the OPEN position (from tile coordinates)
print("Door: _ready_after_setup() called - type: ", type, ", direction: ", direction, ", is_closed: ", is_closed, ", open_position: ", open_position) LogManager.log("Door: _ready_after_setup() called - type: " + str(type) + ", direction: " + str(direction) + ", is_closed: " + str(is_closed) + ", open_position: " + str(open_position), LogManager.CATEGORY_DOOR)
# CRITICAL: Calculate closed position based on direction # CRITICAL: Calculate closed position based on direction
# For StoneDoor/GateDoor: They start OPEN, then CLOSE when entering room # For StoneDoor/GateDoor: They start OPEN, then CLOSE when entering room
@@ -491,7 +524,7 @@ func _ready_after_setup():
# This is used when opening from closed position # This is used when opening from closed position
open_offset = -closed_offset # open_offset = (0, -16) means open is 16px up from closed open_offset = -closed_offset # open_offset = (0, -16) means open is 16px up from closed
print("Door: Calculated positions - open: ", open_position, ", closed: ", closed_position, ", closed_offset: ", closed_offset, ", open_offset: ", open_offset) LogManager.log("Door: Calculated positions - open: " + str(open_position) + ", closed: " + str(closed_position) + ", closed_offset: " + str(closed_offset) + ", open_offset: " + str(open_offset), LogManager.CATEGORY_DOOR)
# CRITICAL: KeyDoors should ALWAYS start closed, regardless of is_closed value # CRITICAL: KeyDoors should ALWAYS start closed, regardless of is_closed value
# KeyDoors should NEVER be moved until opened with a key # KeyDoors should NEVER be moved until opened with a key
@@ -523,7 +556,7 @@ func _ready_after_setup():
position = closed_position position = closed_position
global_position = closed_position global_position = closed_position
set_collision_layer_value(7, true) # Collision enabled when closed set_collision_layer_value(7, true) # Collision enabled when closed
print("Door: KeyDoor starting CLOSED at position ", position, " (direction: ", direction, "), will open to ", closed_position + open_offset, " - collision ENABLED") LogManager.log("Door: KeyDoor starting CLOSED at position " + str(position) + " (direction: " + str(direction) + "), will open to " + str(closed_position + open_offset) + " - collision ENABLED", LogManager.CATEGORY_DOOR)
# Create key indicator sprite for KeyDoor # Create key indicator sprite for KeyDoor
_create_key_indicator() _create_key_indicator()
return # Exit early for KeyDoors return # Exit early for KeyDoors
@@ -533,46 +566,46 @@ func _ready_after_setup():
global_position = closed_position global_position = closed_position
is_closed = true # Ensure state matches position is_closed = true # Ensure state matches position
set_collision_layer_value(7, true) set_collision_layer_value(7, true)
print("Door: Starting CLOSED at position ", position, " (type: ", type, ", direction: ", direction, ") - collision ENABLED") LogManager.log("Door: Starting CLOSED at position " + str(position) + " (type: " + str(type) + ", direction: " + str(direction) + ") - collision ENABLED", LogManager.CATEGORY_DOOR)
else: else:
# StoneDoor/GateDoor starting OPEN (default for blocking doors) # StoneDoor/GateDoor starting OPEN (default for blocking doors)
# CRITICAL: Door MUST start at open position (which is where game_world placed it) # CRITICAL: Door MUST start at open position (which is where game_world placed it)
# Ensure position is EXACTLY at open_position (don't assume game_world set it correctly) # Ensure position is EXACTLY at open_position (don't assume game_world set it correctly)
if position.distance_to(open_position) > 1.0: if position.distance_to(open_position) > 1.0:
# Position doesn't match open_position - force it to open position # Position doesn't match open_position - force it to open position
print("Door: WARNING - Position doesn't match open_position! Forcing to open: ", open_position, " (was: ", position, ")") LogManager.log("Door: WARNING - Position doesn't match open_position! Forcing to open: " + str(open_position) + " (was: " + str(position) + ")", LogManager.CATEGORY_DOOR)
position = open_position position = open_position
global_position = position # Ensure global_position matches position global_position = position # Ensure global_position matches position
is_closed = false # CRITICAL: State MUST be false (open) when at open position is_closed = false # CRITICAL: State MUST be false (open) when at open position
set_collision_layer_value(7, false) # CRITICAL: Collision MUST be DISABLED when open set_collision_layer_value(7, false) # CRITICAL: Collision MUST be DISABLED when open
print("Door: Starting OPEN at position ", position, " (closed: ", closed_position, ", open: ", open_position, ", open_offset: ", open_offset, ", type: ", type, ", direction: ", direction, ") - collision DISABLED, is_closed: ", is_closed) LogManager.log("Door: Starting OPEN at position " + str(position) + " (closed: " + str(closed_position) + ", open: " + str(open_position) + ", open_offset: " + str(open_offset) + ", type: " + str(type) + ", direction: " + str(direction) + ") - collision DISABLED, is_closed: " + str(is_closed), LogManager.CATEGORY_DOOR)
# CRITICAL: Verify the door is actually at open position after setting it # CRITICAL: Verify the door is actually at open position after setting it
var actual_distance = position.distance_to(closed_position) var actual_distance = position.distance_to(closed_position)
var expected_distance = 16.0 # Should be 16 pixels away var expected_distance = 16.0 # Should be 16 pixels away
if abs(actual_distance - expected_distance) > 2.0: if abs(actual_distance - expected_distance) > 2.0:
push_error("Door: ERROR - Door open/closed distance is wrong! Position: ", position, ", closed: ", closed_position, ", distance: ", actual_distance, " (expected: ", expected_distance, ")") LogManager.log_error("Door: ERROR - Door open/closed distance is wrong! Position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(actual_distance) + " (expected: " + str(expected_distance) + ")", LogManager.CATEGORY_DOOR)
# Force it to correct open position # Force it to correct open position
position = open_position position = open_position
global_position = open_position global_position = open_position
is_closed = false # CRITICAL: Ensure state is false when at open position is_closed = false # CRITICAL: Ensure state is false when at open position
set_collision_layer_value(7, false) set_collision_layer_value(7, false)
print("Door: FORCED door to open position: ", position, " (distance to closed: ", position.distance_to(closed_position), ", is_closed: ", is_closed, ")") LogManager.log("Door: FORCED door to open position: " + str(position) + " (distance to closed: " + str(position.distance_to(closed_position)) + ", is_closed: " + str(is_closed) + ")", LogManager.CATEGORY_DOOR)
# FINAL VERIFICATION: Double-check state matches position # FINAL VERIFICATION: Double-check state matches position
var distance_to_closed = position.distance_to(closed_position) var distance_to_closed = position.distance_to(closed_position)
var should_be_open = distance_to_closed > 8.0 # If more than 8px from closed, should be open var should_be_open = distance_to_closed > 8.0 # If more than 8px from closed, should be open
if should_be_open and is_closed: if should_be_open and is_closed:
push_error("Door: ERROR - Door is at open position but is_closed is true! Fixing state...") LogManager.log_error("Door: ERROR - Door is at open position but is_closed is true! Fixing state...", LogManager.CATEGORY_DOOR)
is_closed = false is_closed = false
set_collision_layer_value(7, false) set_collision_layer_value(7, false)
print("Door: Fixed state - door is now OPEN (is_closed: ", is_closed, ", collision: ", get_collision_layer_value(7), ")") LogManager.log("Door: Fixed state - door is now OPEN (is_closed: " + str(is_closed) + ", collision: " + str(get_collision_layer_value(7)) + ")", LogManager.CATEGORY_DOOR)
elif not should_be_open and not is_closed: elif not should_be_open and not is_closed:
push_error("Door: ERROR - Door is at closed position but is_closed is false! Fixing state...") LogManager.log_error("Door: ERROR - Door is at closed position but is_closed is false! Fixing state...", LogManager.CATEGORY_DOOR)
is_closed = true is_closed = true
set_collision_layer_value(7, true) set_collision_layer_value(7, true)
print("Door: Fixed state - door is now CLOSED (is_closed: ", is_closed, ", collision: ", get_collision_layer_value(7), ")") LogManager.log("Door: Fixed state - door is now CLOSED (is_closed: " + str(is_closed) + ", collision: " + str(get_collision_layer_value(7)) + ")", LogManager.CATEGORY_DOOR)
# NOTE: Doors are NOT connected via signals to room triggers # NOTE: Doors are NOT connected via signals to room triggers
# Instead, room triggers call door._on_room_entered() directly # Instead, room triggers call door._on_room_entered() directly
@@ -634,20 +667,20 @@ func _on_room_entered(body):
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0 var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
var is_actually_open = distance_to_closed > 5.0 # If door is more than 5 pixels away from closed_position, it's open var is_actually_open = distance_to_closed > 5.0 # If door is more than 5 pixels away from closed_position, it's open
print("Door: _on_room_entered() - type: ", type, ", is_closed: ", is_closed, ", is_actually_open: ", is_actually_open, ", position: ", position, ", closed: ", closed_position, ", distance: ", distance_to_closed) LogManager.log("Door: _on_room_entered() - type: " + str(type) + ", is_closed: " + str(is_closed) + ", is_actually_open: " + str(is_actually_open) + ", position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(distance_to_closed), LogManager.CATEGORY_DOOR)
# CRITICAL: Only close if door is actually open (both flag and position must indicate open) # CRITICAL: Only close if door is actually open (both flag and position must indicate open)
# If door is already closed, don't do anything # If door is already closed, don't do anything
if is_actually_open and not is_closing and not is_opening: if is_actually_open and not is_closing and not is_opening:
# Door is actually open (position is away from closed position) - close it # Door is actually open (position is away from closed position) - close it
print("Door: Closing door on room entry - was at position ", position, " (closed: ", closed_position, ", is_closed: ", is_closed, ", distance: ", distance_to_closed, ")") LogManager.log("Door: Closing door on room entry - was at position " + str(position) + " (closed: " + str(closed_position) + ", is_closed: " + str(is_closed) + ", distance: " + str(distance_to_closed) + ")", LogManager.CATEGORY_DOOR)
# Ensure door is at open position before closing # Ensure door is at open position before closing
var expected_open_pos = closed_position + open_offset var expected_open_pos = closed_position + open_offset
var dist_to_open = position.distance_to(expected_open_pos) var dist_to_open = position.distance_to(expected_open_pos)
if dist_to_open > 5.0: if dist_to_open > 5.0:
# Door is not at expected open position - reset to open position first # Door is not at expected open position - reset to open position first
print("Door: WARNING - Door is not at expected open position! Resetting to open: ", expected_open_pos, " (was at: ", position, ")") LogManager.log("Door: WARNING - Door is not at expected open position! Resetting to open: " + str(expected_open_pos) + " (was at: " + str(position) + ")", LogManager.CATEGORY_DOOR)
position = expected_open_pos position = expected_open_pos
global_position = expected_open_pos global_position = expected_open_pos
is_closed = false is_closed = false
@@ -659,20 +692,20 @@ func _on_room_entered(body):
return # Exit early, don't check puzzle state yet return # Exit early, don't check puzzle state yet
elif is_actually_open: elif is_actually_open:
# Door is open but animation already in progress - don't interfere # Door is open but animation already in progress - don't interfere
print("Door: Door is open but animation in progress, not closing") LogManager.log("Door: Door is open but animation in progress, not closing", LogManager.CATEGORY_DOOR)
return return
elif not is_actually_open: elif not is_actually_open:
# Door is already at closed position - but for StoneDoor/GateDoor, this shouldn't happen on room entry # Door is already at closed position - but for StoneDoor/GateDoor, this shouldn't happen on room entry
# They should start OPEN and then CLOSE when entering room # They should start OPEN and then CLOSE when entering room
# If door is at closed position, it might have been closed already - don't do anything # If door is at closed position, it might have been closed already - don't do anything
print("Door: WARNING - Door is already at closed position when entering room! This shouldn't happen for StoneDoor/GateDoor that start open.") LogManager.log("Door: WARNING - Door is already at closed position when entering room! This shouldn't happen for StoneDoor/GateDoor that start open.", LogManager.CATEGORY_DOOR)
if closed_position != Vector2.ZERO: if closed_position != Vector2.ZERO:
# Ensure exact position and state match # Ensure exact position and state match
position = closed_position position = closed_position
global_position = closed_position global_position = closed_position
is_closed = true is_closed = true
set_collision_layer_value(7, true) # Collision ENABLED when closed set_collision_layer_value(7, true) # Collision ENABLED when closed
print("Door: Door was already closed - ensuring state is correct, position: ", position, ", closed: ", closed_position) LogManager.log("Door: Door was already closed - ensuring state is correct, position: " + str(position) + ", closed: " + str(closed_position), LogManager.CATEGORY_DOOR)
# Now that door is confirmed closed, check if puzzle is already solved # Now that door is confirmed closed, check if puzzle is already solved
# CRITICAL: Only check puzzle state if door is closed - don't check if puzzle is already solved # CRITICAL: Only check puzzle state if door is closed - don't check if puzzle is already solved
if not puzzle_solved: if not puzzle_solved:
@@ -694,7 +727,7 @@ func _check_puzzle_state():
# CRITICAL: Don't check puzzle state while door is animating (closing or opening) # CRITICAL: Don't check puzzle state while door is animating (closing or opening)
# This prevents race conditions where switch triggers before door finishes closing # This prevents race conditions where switch triggers before door finishes closing
if is_closing or is_opening: if is_closing or is_opening:
print("Door: Skipping puzzle check - door is animating (is_closing: ", is_closing, ", is_opening: ", is_opening, ")") LogManager.log("Door: Skipping puzzle check - door is animating (is_closing: " + str(is_closing) + ", is_opening: " + str(is_opening) + ")", LogManager.CATEGORY_DOOR)
return return
# Check door's actual state (position-based check is more reliable than flags) # Check door's actual state (position-based check is more reliable than flags)
@@ -708,13 +741,15 @@ func _check_puzzle_state():
# This handles race conditions where switch triggers while door is still closing # This handles race conditions where switch triggers while door is still closing
if puzzle_solved and (not is_actually_open or collision_enabled): if puzzle_solved and (not is_actually_open or collision_enabled):
# Door should be open but isn't (position or collision) - reset puzzle_solved to allow switch to trigger again # Door should be open but isn't (position or collision) - reset puzzle_solved to allow switch to trigger again
print("Door: puzzle_solved is true but door is not actually open (position: ", is_actually_open, ", collision: ", collision_enabled, ") - resetting to allow switch to trigger again") LogManager.log("Door: puzzle_solved is true but door is not actually open (position: " + str(is_actually_open) + ", collision: " + str(collision_enabled) + ") - resetting to allow switch to trigger again", LogManager.CATEGORY_DOOR)
puzzle_solved = false puzzle_solved = false
switches_activated = false switches_activated = false
# Check if all enemies are defeated (enemies in blocking room) # Check if all enemies are defeated (enemies in blocking room)
if requires_enemies and _are_all_enemies_defeated(): if requires_enemies and _are_all_enemies_defeated():
print("Door: All enemies defeated! Opening door ", name, " (type: ", type, ", room: ", blocking_room.get("x", "?") if blocking_room and not blocking_room.is_empty() else "?", ",", blocking_room.get("y", "?") if blocking_room and not blocking_room.is_empty() else "?", ")") var room_x_str = str(blocking_room.get("x", "?")) if blocking_room and not blocking_room.is_empty() else "?"
var room_y_str = str(blocking_room.get("y", "?")) if blocking_room and not blocking_room.is_empty() else "?"
LogManager.log("Door: All enemies defeated! Opening door " + str(name) + " (type: " + str(type) + ", room: " + room_x_str + "," + room_y_str + ")", LogManager.CATEGORY_DOOR)
enemies_defeated = true enemies_defeated = true
puzzle_solved = true puzzle_solved = true
if is_actually_closed: if is_actually_closed:
@@ -744,7 +779,7 @@ func _check_puzzle_state():
# Not all switches are active # Not all switches are active
if puzzle_solved and has_pillar_switch: if puzzle_solved and has_pillar_switch:
# Pillar switch became inactive and door was open - close it and reset puzzle # Pillar switch became inactive and door was open - close it and reset puzzle
print("Door: Pillar switch deactivated - closing door ", name) LogManager.log("Door: Pillar switch deactivated - closing door " + str(name), LogManager.CATEGORY_DOOR)
switches_activated = false switches_activated = false
puzzle_solved = false puzzle_solved = false
if not is_actually_closed: if not is_actually_closed:
@@ -768,9 +803,13 @@ func _are_all_enemies_defeated() -> bool:
return false return false
# Find all enemies in the room that were spawned from spawners # Find all enemies in the room that were spawned from spawners
var entities_node = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
var entities_node = null
if game_world:
entities_node = game_world.get_node_or_null("Entities")
if not entities_node: if not entities_node:
entities_node = get_node("/root/GameWorld/Entities") # Fallback without throwing if GameWorld isn't ready yet
entities_node = get_node_or_null("/root/GameWorld/Entities")
if not entities_node: if not entities_node:
return false return false
@@ -804,10 +843,14 @@ func _are_all_enemies_defeated() -> bool:
if enemy_in_room: if enemy_in_room:
room_spawned_enemies.append(child) room_spawned_enemies.append(child)
print("Door: Found spawned enemy in room: ", child.name, " (spawner: ", child.get_meta("spawner_name") if child.has_meta("spawner_name") else "unknown", ", is_dead: ", child.is_dead if "is_dead" in child else "unknown", ")") var spawner_name = str(child.get_meta("spawner_name")) if child.has_meta("spawner_name") else "unknown"
var is_dead_str = str(child.is_dead) if "is_dead" in child else "unknown"
LogManager.log("Door: Found spawned enemy in room: " + str(child.name) + " (spawner: " + spawner_name + ", is_dead: " + is_dead_str + ")", LogManager.CATEGORY_DOOR)
# Check if all spawned enemies are dead # Check if all spawned enemies are dead
print("Door: _are_all_enemies_defeated() - Found ", room_spawned_enemies.size(), " spawned enemies in room (", target_room.get("x", "?") if target_room and not target_room.is_empty() else "?", ",", target_room.get("y", "?") if target_room and not target_room.is_empty() else "?", ")") var target_room_x_str = str(target_room.get("x", "?")) if target_room and not target_room.is_empty() else "?"
var target_room_y_str = str(target_room.get("y", "?")) if target_room and not target_room.is_empty() else "?"
LogManager.log("Door: _are_all_enemies_defeated() - Found " + str(room_spawned_enemies.size()) + " spawned enemies in room (" + target_room_x_str + "," + target_room_y_str + ")", LogManager.CATEGORY_DOOR)
# First, check if any enemies in room_spawned_enemies are still alive # First, check if any enemies in room_spawned_enemies are still alive
# If any are alive, puzzle is not solved # If any are alive, puzzle is not solved
@@ -820,12 +863,12 @@ func _are_all_enemies_defeated() -> bool:
enemy_is_dead = enemy.is_queued_for_deletion() or not enemy.is_inside_tree() enemy_is_dead = enemy.is_queued_for_deletion() or not enemy.is_inside_tree()
if not enemy_is_dead: if not enemy_is_dead:
print("Door: Enemy ", enemy.name, " is still alive - puzzle not solved yet") LogManager.log("Door: Enemy " + str(enemy.name) + " is still alive - puzzle not solved yet", LogManager.CATEGORY_DOOR)
return false # Enemy is still alive, puzzle not solved return false # Enemy is still alive, puzzle not solved
# If we have enemies and all are dead, puzzle is solved # If we have enemies and all are dead, puzzle is solved
if room_spawned_enemies.size() > 0: if room_spawned_enemies.size() > 0:
print("Door: All ", room_spawned_enemies.size(), " spawned enemies are dead! Puzzle solved!") LogManager.log("Door: All " + str(room_spawned_enemies.size()) + " spawned enemies are dead! Puzzle solved!", LogManager.CATEGORY_DOOR)
return true # All enemies found are dead return true # All enemies found are dead
# No spawned enemies found - check if spawners have actually spawned enemies before # No spawned enemies found - check if spawners have actually spawned enemies before
@@ -836,9 +879,9 @@ func _are_all_enemies_defeated() -> bool:
# This catches cases where enemies weren't added to room_spawned_enemies due to position check issues # This catches cases where enemies weren't added to room_spawned_enemies due to position check issues
var entities_child = entities_node.get_node_or_null("Entities") if entities_node else null var entities_child = entities_node.get_node_or_null("Entities") if entities_node else null
if not entities_child and entities_node: if not entities_child and entities_node:
var game_world = get_tree().get_first_node_in_group("game_world") var fallback_game_world = get_tree().get_first_node_in_group("game_world")
if game_world: if fallback_game_world:
entities_child = game_world.get_node_or_null("Entities") entities_child = fallback_game_world.get_node_or_null("Entities")
var tile_size = 16 var tile_size = 16
var room_min_x = target_room.x + 2 var room_min_x = target_room.x + 2
@@ -871,7 +914,7 @@ func _are_all_enemies_defeated() -> bool:
if enemy_is_alive: if enemy_is_alive:
# Found an ALIVE enemy in this room - puzzle not solved! # Found an ALIVE enemy in this room - puzzle not solved!
print("Door: Found ALIVE enemy ", child.name, " in room - puzzle not solved yet (enemy still alive)") LogManager.log("Door: Found ALIVE enemy " + str(child.name) + " in room - puzzle not solved yet (enemy still alive)", LogManager.CATEGORY_DOOR)
return false return false
# No alive enemies found in room - now check if spawners have spawned # No alive enemies found in room - now check if spawners have spawned
@@ -907,9 +950,9 @@ func _are_all_enemies_defeated() -> bool:
if not has_spawned: if not has_spawned:
var entities_child_for_spawner = entities_node.get_node_or_null("Entities") if entities_node else null var entities_child_for_spawner = entities_node.get_node_or_null("Entities") if entities_node else null
if not entities_child_for_spawner and entities_node: if not entities_child_for_spawner and entities_node:
var game_world = get_tree().get_first_node_in_group("game_world") var fallback_game_world = get_tree().get_first_node_in_group("game_world")
if game_world: if fallback_game_world:
entities_child_for_spawner = game_world.get_node_or_null("Entities") entities_child_for_spawner = fallback_game_world.get_node_or_null("Entities")
if entities_child_for_spawner: if entities_child_for_spawner:
for child in entities_child_for_spawner.get_children(): for child in entities_child_for_spawner.get_children():
@@ -1002,7 +1045,7 @@ func _are_all_enemies_defeated() -> bool:
for spawner_name in unique_spawner_names_that_spawned.keys(): for spawner_name in unique_spawner_names_that_spawned.keys():
spawners_in_room.append(null) # Placeholder for destroyed spawner spawners_in_room.append(null) # Placeholder for destroyed spawner
spawners_that_have_spawned.append(null) # Count as spawned spawners_that_have_spawned.append(null) # Count as spawned
print("Door: Spawner ", spawner_name, " was destroyed but spawned enemies that are now all dead - counting as spawned") LogManager.log("Door: Spawner " + str(spawner_name) + " was destroyed but spawned enemies that are now all dead - counting as spawned", LogManager.CATEGORY_DOOR)
else: else:
# Spawners exist - check if any weren't counted as spawned yet # Spawners exist - check if any weren't counted as spawned yet
for spawner_name in unique_spawner_names_that_spawned.keys(): for spawner_name in unique_spawner_names_that_spawned.keys():
@@ -1017,7 +1060,7 @@ func _are_all_enemies_defeated() -> bool:
var spawner = spawners_in_room[i] var spawner = spawners_in_room[i]
if is_instance_valid(spawner) and spawner.name == spawner_name: if is_instance_valid(spawner) and spawner.name == spawner_name:
spawners_that_have_spawned.append(spawner) spawners_that_have_spawned.append(spawner)
print("Door: Found dead enemy from spawner ", spawner_name, " - marking as spawned") LogManager.log("Door: Found dead enemy from spawner " + str(spawner_name) + " - marking as spawned", LogManager.CATEGORY_DOOR)
break break
@@ -1034,7 +1077,7 @@ func _are_all_enemies_defeated() -> bool:
if valid_spawners_count > 0 and valid_spawned_count >= valid_spawners_count: if valid_spawners_count > 0 and valid_spawned_count >= valid_spawners_count:
# All spawners in room have spawned at least once, and no enemies found in room # All spawners in room have spawned at least once, and no enemies found in room
# This means all spawned enemies are dead - puzzle solved! # This means all spawned enemies are dead - puzzle solved!
print("Door: No spawned enemies found, but all ", valid_spawners_count, " spawners in room have spawned enemies that are all dead - puzzle solved!") LogManager.log("Door: No spawned enemies found, but all " + str(valid_spawners_count) + " spawners in room have spawned enemies that are all dead - puzzle solved!", LogManager.CATEGORY_DOOR)
return true return true
# Also check: if no spawners found (they were destroyed), but this is a puzzle room (has blocking doors), # Also check: if no spawners found (they were destroyed), but this is a puzzle room (has blocking doors),
@@ -1043,13 +1086,13 @@ func _are_all_enemies_defeated() -> bool:
if valid_spawners_count == 0 and valid_spawned_count > 0: if valid_spawners_count == 0 and valid_spawned_count > 0:
# Spawners were destroyed, but we found evidence they spawned # Spawners were destroyed, but we found evidence they spawned
# Since no enemies found, they must all be dead - puzzle solved! # Since no enemies found, they must all be dead - puzzle solved!
print("Door: No spawners or enemies found, but found evidence of spawned enemies that are now all dead - puzzle solved!") LogManager.log("Door: No spawners or enemies found, but found evidence of spawned enemies that are now all dead - puzzle solved!", LogManager.CATEGORY_DOOR)
return true return true
if valid_spawners_count > 0: if valid_spawners_count > 0:
print("Door: Spawners in room (", valid_spawners_count, ") but only ", valid_spawned_count, " have spawned - puzzle not solved yet") LogManager.log("Door: Spawners in room (" + str(valid_spawners_count) + ") but only " + str(valid_spawned_count) + " have spawned - puzzle not solved yet", LogManager.CATEGORY_DOOR)
else: else:
print("Door: No spawned enemies found in room - puzzle not solved yet (enemies may not have spawned or already removed)") LogManager.log("Door: No spawned enemies found in room - puzzle not solved yet (enemies may not have spawned or already removed)", LogManager.CATEGORY_DOOR)
return false return false
for enemy in room_spawned_enemies: for enemy in room_spawned_enemies:
@@ -1062,13 +1105,13 @@ func _are_all_enemies_defeated() -> bool:
enemy_is_dead = enemy.is_queued_for_deletion() or not enemy.is_inside_tree() enemy_is_dead = enemy.is_queued_for_deletion() or not enemy.is_inside_tree()
if not enemy_is_dead: if not enemy_is_dead:
print("Door: Enemy ", enemy.name, " is still alive (is_dead: ", enemy_is_dead, ", is_queued: ", enemy.is_queued_for_deletion(), ", in_tree: ", enemy.is_inside_tree(), ")") LogManager.log("Door: Enemy " + str(enemy.name) + " is still alive (is_dead: " + str(enemy_is_dead) + ", is_queued: " + str(enemy.is_queued_for_deletion()) + ", in_tree: " + str(enemy.is_inside_tree()) + ")", LogManager.CATEGORY_DOOR)
return false return false
else: else:
# Enemy is no longer valid (removed from scene) - consider it dead # Enemy is no longer valid (removed from scene) - consider it dead
print("Door: Enemy is no longer valid (removed from scene) - counting as dead") LogManager.log("Door: Enemy is no longer valid (removed from scene) - counting as dead", LogManager.CATEGORY_DOOR)
print("Door: All ", room_spawned_enemies.size(), " spawned enemies are dead! Puzzle solved!") LogManager.log("Door: All " + str(room_spawned_enemies.size()) + " spawned enemies are dead! Puzzle solved!", LogManager.CATEGORY_DOOR)
return true # All enemies are dead return true # All enemies are dead
func _spawn_smoke_puffs_on_close(): func _spawn_smoke_puffs_on_close():
@@ -1135,20 +1178,22 @@ func _are_all_switches_activated() -> bool:
# Do NOT use position-based fallback checks - they cause cross-room door triggering! # Do NOT use position-based fallback checks - they cause cross-room door triggering!
if connected_switches.size() > 0: if connected_switches.size() > 0:
# Check all connected switches (these are the switches in THIS door's puzzle room) # Check all connected switches (these are the switches in THIS door's puzzle room)
print("Door: _are_all_switches_activated() - Checking ", connected_switches.size(), " connected switches for door ", name, " (room: ", blocking_room.get("x", "?"), ",", blocking_room.get("y", "?"), ")") var switch_room_x = str(blocking_room.get("x", "?")) if blocking_room and not blocking_room.is_empty() else "?"
var switch_room_y = str(blocking_room.get("y", "?")) if blocking_room and not blocking_room.is_empty() else "?"
LogManager.log("Door: _are_all_switches_activated() - Checking " + str(connected_switches.size()) + " connected switches for door " + str(name) + " (room: " + switch_room_x + "," + switch_room_y + ")", LogManager.CATEGORY_DOOR)
for switch in connected_switches: for switch in connected_switches:
if not is_instance_valid(switch): if not is_instance_valid(switch):
continue continue
# is_activated is a variable, not a method # is_activated is a variable, not a method
if not switch.is_activated: if not switch.is_activated:
print("Door: Switch ", switch.name, " is NOT activated") LogManager.log("Door: Switch " + str(switch.name) + " is NOT activated", LogManager.CATEGORY_DOOR)
return false return false
print("Door: All connected switches are activated!") LogManager.log("Door: All connected switches are activated!", LogManager.CATEGORY_DOOR)
return true # All connected switches are activated return true # All connected switches are activated
# CRITICAL: If no switches are connected, the puzzle is NOT solved! # CRITICAL: If no switches are connected, the puzzle is NOT solved!
# Switches should ALWAYS be connected when spawned - if they're not, it's an error # Switches should ALWAYS be connected when spawned - if they're not, it's an error
print("Door: WARNING - Door ", name, " has no connected switches! Puzzle cannot be solved!") LogManager.log("Door: WARNING - Door " + str(name) + " has no connected switches! Puzzle cannot be solved!", LogManager.CATEGORY_DOOR)
return false # No connected switches means puzzle is NOT solved return false # No connected switches means puzzle is NOT solved
func _on_key_interaction_area_body_entered(body): func _on_key_interaction_area_body_entered(body):
@@ -1165,7 +1210,7 @@ func _on_key_interaction_area_body_entered(body):
key_used = true key_used = true
_show_key_indicator() _show_key_indicator()
_open() _open()
print("KeyDoor opened with key!") LogManager.log("KeyDoor opened with key!", LogManager.CATEGORY_DOOR)
func _show_key_indicator(): func _show_key_indicator():
# Show key indicator above door # Show key indicator above door
@@ -1253,7 +1298,8 @@ func _sync_door_open():
else: else:
$SfxOpenStoneDoor.play() $SfxOpenStoneDoor.play()
print("Door: Client received door open RPC for ", name, " - starting open animation", " (key_used=", key_used, ")" if type == "KeyDoor" else "") var key_used_str = " (key_used=" + str(key_used) + ")" if type == "KeyDoor" else ""
LogManager.log("Door: Client received door open RPC for " + str(name) + " - starting open animation" + key_used_str, LogManager.CATEGORY_DOOR)
@rpc("authority", "reliable") @rpc("authority", "reliable")
func _sync_puzzle_solved(is_solved: bool): func _sync_puzzle_solved(is_solved: bool):
@@ -1263,7 +1309,7 @@ func _sync_puzzle_solved(is_solved: bool):
if is_solved: if is_solved:
enemies_defeated = true enemies_defeated = true
switches_activated = true switches_activated = true
print("Door: Client received puzzle_solved sync for ", name, " - puzzle_solved: ", is_solved) LogManager.log("Door: Client received puzzle_solved sync for " + str(name) + " - puzzle_solved: " + str(is_solved), LogManager.CATEGORY_DOOR)
@rpc("authority", "reliable") @rpc("authority", "reliable")
func _sync_door_close(): func _sync_door_close():
@@ -1289,4 +1335,4 @@ func _sync_door_close():
is_closing = true is_closing = true
move_timer = 0.0 move_timer = 0.0
print("Door: Client received door close RPC for ", name, " - starting close animation") LogManager.log("Door: Client received door close RPC for " + str(name) + " - starting close animation", LogManager.CATEGORY_DOOR)

View File

@@ -96,7 +96,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
# Calculate target room count based on level # Calculate target room count based on level
# Level 1: 7-8 rooms, then increase by 2-3 rooms per level # Level 1: 7-8 rooms, then increase by 2-3 rooms per level
var target_room_count = 7 + (level - 1) * 2 + rng.randi_range(0, 1) # Level 1: 7-8, Level 2: 9-10, etc. var target_room_count = 7 + (level - 1) * 2 + rng.randi_range(0, 1) # Level 1: 7-8, Level 2: 9-10, etc.
print("DungeonGenerator: Level ", level, " - Target room count: ", target_room_count) LogManager.log("DungeonGenerator: Level " + str(level) + " - Target room count: " + str(target_room_count), LogManager.CATEGORY_DUNGEON)
# Initialize grid (0 = wall, 1 = floor, 2 = door, 3 = corridor) # Initialize grid (0 = wall, 1 = floor, 2 = door, 3 = corridor)
var grid = [] var grid = []
@@ -137,7 +137,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
attempts -= 1 attempts -= 1
print("DungeonGenerator: Generated ", all_rooms.size(), " rooms (target was ", target_room_count, ")") LogManager.log("DungeonGenerator: Generated " + str(all_rooms.size()) + " rooms (target was " + str(target_room_count) + ")", LogManager.CATEGORY_DUNGEON)
# 3. Connect rooms with corridors/doors # 3. Connect rooms with corridors/doors
if all_rooms.size() > 1: if all_rooms.size() > 1:
@@ -156,7 +156,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
# 6. Mark exit room (farthest REACHABLE room from start) # 6. Mark exit room (farthest REACHABLE room from start)
# First find all reachable rooms from start # First find all reachable rooms from start
var reachable_rooms = _find_reachable_rooms(all_rooms[start_room_index], all_rooms, all_doors) var reachable_rooms = _find_reachable_rooms(all_rooms[start_room_index], all_rooms, all_doors)
print("DungeonGenerator: Found ", reachable_rooms.size(), " reachable rooms from start (out of ", all_rooms.size(), " total)") LogManager.log("DungeonGenerator: Found " + str(reachable_rooms.size()) + " reachable rooms from start (out of " + str(all_rooms.size()) + " total)", LogManager.CATEGORY_DUNGEON)
# CRITICAL: Remove inaccessible rooms (rooms not reachable from start) # CRITICAL: Remove inaccessible rooms (rooms not reachable from start)
# Store the start room before filtering (it should always be reachable) # Store the start room before filtering (it should always be reachable)
@@ -179,13 +179,13 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
filtered_rooms.append(room) filtered_rooms.append(room)
else: else:
inaccessible_count += 1 inaccessible_count += 1
print("DungeonGenerator: Removing inaccessible room at (", room.x, ", ", room.y, ") - no corridor connection") LogManager.log("DungeonGenerator: Removing inaccessible room at (" + str(room.x) + ", " + str(room.y) + ") - no corridor connection", LogManager.CATEGORY_DUNGEON)
# Update all_rooms to only include reachable rooms # Update all_rooms to only include reachable rooms
all_rooms = filtered_rooms all_rooms = filtered_rooms
if inaccessible_count > 0: if inaccessible_count > 0:
print("DungeonGenerator: Removed ", inaccessible_count, " inaccessible room(s). Remaining rooms: ", all_rooms.size()) LogManager.log("DungeonGenerator: Removed " + str(inaccessible_count) + " inaccessible room(s). Remaining rooms: " + str(all_rooms.size()), LogManager.CATEGORY_DUNGEON)
# Update start_room_index after filtering (find start room in new array using value-based comparison) # Update start_room_index after filtering (find start room in new array using value-based comparison)
start_room_index = -1 start_room_index = -1
@@ -197,7 +197,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
break break
if start_room_index == -1: if start_room_index == -1:
push_error("DungeonGenerator: ERROR - Start room was removed! This should never happen!") LogManager.log_error("DungeonGenerator: ERROR - Start room was removed! This should never happen!", LogManager.CATEGORY_DUNGEON)
start_room_index = 0 # Fallback start_room_index = 0 # Fallback
# Also remove doors connected to inaccessible rooms (clean up all_doors) # Also remove doors connected to inaccessible rooms (clean up all_doors)
@@ -229,23 +229,23 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
filtered_doors.append(door) filtered_doors.append(door)
else: else:
doors_removed += 1 doors_removed += 1
print("DungeonGenerator: Removing door - room1 reachable: ", door_room1_reachable, ", room2 reachable: ", door_room2_reachable) LogManager.log("DungeonGenerator: Removing door - room1 reachable: " + str(door_room1_reachable) + ", room2 reachable: " + str(door_room2_reachable), LogManager.CATEGORY_DUNGEON)
all_doors = filtered_doors all_doors = filtered_doors
if doors_removed > 0: if doors_removed > 0:
print("DungeonGenerator: Removed ", doors_removed, " door(s) connected to inaccessible rooms. Remaining doors: ", all_doors.size()) LogManager.log("DungeonGenerator: Removed " + str(doors_removed) + " door(s) connected to inaccessible rooms. Remaining doors: " + str(all_doors.size()), LogManager.CATEGORY_DUNGEON)
# Find the farthest reachable room (now all rooms are reachable, but find farthest) # Find the farthest reachable room (now all rooms are reachable, but find farthest)
# Make sure we have at least 2 rooms (start and exit must be different) # Make sure we have at least 2 rooms (start and exit must be different)
# exit_room_index is already declared at function level # exit_room_index is already declared at function level
if all_rooms.size() < 2: if all_rooms.size() < 2:
push_error("DungeonGenerator: ERROR - Not enough reachable rooms! Need at least 2 (start + exit), but only have ", all_rooms.size()) LogManager.log_error("DungeonGenerator: ERROR - Not enough reachable rooms! Need at least 2 (start + exit), but only have " + str(all_rooms.size()), LogManager.CATEGORY_DUNGEON)
# Use start room as exit if only one room exists (shouldn't happen, but handle gracefully) # Use start room as exit if only one room exists (shouldn't happen, but handle gracefully)
if all_rooms.size() == 1: if all_rooms.size() == 1:
exit_room_index = 0 exit_room_index = 0
else: else:
# No rooms at all - this is a critical error # No rooms at all - this is a critical error
push_error("DungeonGenerator: CRITICAL ERROR - No rooms left after filtering!") LogManager.log_error("DungeonGenerator: CRITICAL ERROR - No rooms left after filtering!", LogManager.CATEGORY_DUNGEON)
return {} # Return empty dungeon return {} # Return empty dungeon
else: else:
exit_room_index = _find_farthest_room(all_rooms, start_room_index) exit_room_index = _find_farthest_room(all_rooms, start_room_index)
@@ -266,7 +266,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
exit_room_index = second_farthest exit_room_index = second_farthest
all_rooms[exit_room_index].modifiers.append({"type": "EXIT"}) all_rooms[exit_room_index].modifiers.append({"type": "EXIT"})
print("DungeonGenerator: Selected exit room at index ", exit_room_index, " position: ", all_rooms[exit_room_index].x, ",", all_rooms[exit_room_index].y) LogManager.log("DungeonGenerator: Selected exit room at index " + str(exit_room_index) + " position: " + str(all_rooms[exit_room_index].x) + "," + str(all_rooms[exit_room_index].y), LogManager.CATEGORY_DUNGEON)
# 7. Render walls around rooms # 7. Render walls around rooms
_render_room_walls(all_rooms, grid, tile_grid, map_size, rng) _render_room_walls(all_rooms, grid, tile_grid, map_size, rng)
@@ -274,12 +274,12 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
# 7.5. Place stairs in exit room BEFORE placing torches (so torches don't overlap stairs) # 7.5. Place stairs in exit room BEFORE placing torches (so torches don't overlap stairs)
var stairs_data = _place_stairs_in_exit_room(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng) var stairs_data = _place_stairs_in_exit_room(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng)
if stairs_data.is_empty(): if stairs_data.is_empty():
print("DungeonGenerator: ERROR - Failed to place stairs in exit room! Room size: ", all_rooms[exit_room_index].w, "x", all_rooms[exit_room_index].h, " Doors: ", all_doors.size()) LogManager.log_error("DungeonGenerator: ERROR - Failed to place stairs in exit room! Room size: " + str(all_rooms[exit_room_index].w) + "x" + str(all_rooms[exit_room_index].h) + " Doors: " + str(all_doors.size()), LogManager.CATEGORY_DUNGEON)
# CRITICAL: Force place stairs - we MUST have an exit! # CRITICAL: Force place stairs - we MUST have an exit!
print("DungeonGenerator: FORCING stairs placement in exit room center") LogManager.log("DungeonGenerator: FORCING stairs placement in exit room center", LogManager.CATEGORY_DUNGEON)
stairs_data = _force_place_stairs(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng) stairs_data = _force_place_stairs(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng)
if stairs_data.is_empty(): if stairs_data.is_empty():
push_error("DungeonGenerator: CRITICAL ERROR - Could not place stairs even with force placement!") LogManager.log_error("DungeonGenerator: CRITICAL ERROR - Could not place stairs even with force placement!", LogManager.CATEGORY_DUNGEON)
# 8. Place torches in rooms (AFTER stairs, so torches don't overlap stairs) # 8. Place torches in rooms (AFTER stairs, so torches don't overlap stairs)
var all_torches = [] var all_torches = []
@@ -309,7 +309,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
break break
if not already_in_list: if not already_in_list:
rooms_with_spawner_puzzles.append(puzzle_room) rooms_with_spawner_puzzles.append(puzzle_room)
print("DungeonGenerator: Room (", puzzle_room.x, ", ", puzzle_room.y, ") has monster spawner puzzle - will skip pre-spawning enemies") LogManager.log("DungeonGenerator: Room (" + str(puzzle_room.x) + ", " + str(puzzle_room.y) + ") has monster spawner puzzle - will skip pre-spawning enemies", LogManager.CATEGORY_DUNGEON)
# 9. Place enemies in rooms (scaled by level, excluding start and exit rooms, and rooms with spawner puzzles) # 9. Place enemies in rooms (scaled by level, excluding start and exit rooms, and rooms with spawner puzzles)
var all_enemies = [] var all_enemies = []
@@ -323,7 +323,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
if spawner_room.x == room.x and spawner_room.y == room.y and \ if spawner_room.x == room.x and spawner_room.y == room.y and \
spawner_room.w == room.w and spawner_room.h == room.h: spawner_room.w == room.w and spawner_room.h == room.h:
has_spawner_puzzle = true has_spawner_puzzle = true
print("DungeonGenerator: Skipping pre-spawned enemies for room (", room.x, ", ", room.y, ") - has monster spawner puzzle") LogManager.log("DungeonGenerator: Skipping pre-spawned enemies for room (" + str(room.x) + ", " + str(room.y) + ") - has monster spawner puzzle", LogManager.CATEGORY_DUNGEON)
break break
if not has_spawner_puzzle: if not has_spawner_puzzle:
@@ -1317,7 +1317,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
# Choose a random wall to place stairs on (excluding corners) # Choose a random wall to place stairs on (excluding corners)
# Make sure stairs don't overlap any doors # Make sure stairs don't overlap any doors
# Returns stairs data with position and size for Area2D creation # Returns stairs data with position and size for Area2D creation
print("DungeonGenerator: Placing stairs in exit room: ", exit_room.x, ",", exit_room.y, " size: ", exit_room.w, "x", exit_room.h, " doors: ", all_doors.size()) LogManager.log("DungeonGenerator: Placing stairs in exit room: " + str(exit_room.x) + "," + str(exit_room.y) + " size: " + str(exit_room.w) + "x" + str(exit_room.h) + " doors: " + str(all_doors.size()), LogManager.CATEGORY_DUNGEON)
var stairs_data: Dictionary = {} var stairs_data: Dictionary = {}
var wall_choices = [] var wall_choices = []
@@ -1334,7 +1334,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
for stairs_tile_y in range(stairs_y, stairs_y + stairs_h): for stairs_tile_y in range(stairs_y, stairs_y + stairs_h):
if stairs_tile_x >= 0 and stairs_tile_x < map_size.x and stairs_tile_y >= 0 and stairs_tile_y < map_size.y: if stairs_tile_x >= 0 and stairs_tile_x < map_size.x and stairs_tile_y >= 0 and stairs_tile_y < map_size.y:
if grid[stairs_tile_x][stairs_tile_y] == 2: # Grid value 2 = door if grid[stairs_tile_x][stairs_tile_y] == 2: # Grid value 2 = door
print("DungeonGenerator: Stairs tile (", stairs_tile_x, ",", stairs_tile_y, ") is marked as door in grid!") LogManager.log("DungeonGenerator: Stairs tile (" + str(stairs_tile_x) + "," + str(stairs_tile_y) + ") is marked as door in grid!", LogManager.CATEGORY_DUNGEON)
return true return true
# SECOND: Check door dictionary - verify against all known doors # SECOND: Check door dictionary - verify against all known doors
@@ -1377,7 +1377,8 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
# Check if this stairs tile matches any door tile # Check if this stairs tile matches any door tile
for door_tile in door_tiles: for door_tile in door_tiles:
if stairs_tile_x == door_tile.x and stairs_tile_y == door_tile.y: if stairs_tile_x == door_tile.x and stairs_tile_y == door_tile.y:
print("DungeonGenerator: Stairs tile (", stairs_tile_x, ",", stairs_tile_y, ") overlaps door tile (", door_tile.x, ",", door_tile.y, ") from door at (", door_x, ",", door_y, ") dir: ", door.dir if "dir" in door else "unknown") var door_dir_str = str(door.dir) if "dir" in door else "unknown"
LogManager.log("DungeonGenerator: Stairs tile (" + str(stairs_tile_x) + "," + str(stairs_tile_y) + ") overlaps door tile (" + str(door_tile.x) + "," + str(door_tile.y) + ") from door at (" + str(door_x) + "," + str(door_y) + ") dir: " + door_dir_str, LogManager.CATEGORY_DUNGEON)
return true return true
return false return false
@@ -1453,7 +1454,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
}) })
if wall_choices.size() == 0: if wall_choices.size() == 0:
print("DungeonGenerator: ERROR - No valid walls for stairs! Exit room too small: ", exit_room.w, "x", exit_room.h) LogManager.log_error("DungeonGenerator: ERROR - No valid walls for stairs! Exit room too small: " + str(exit_room.w) + "x" + str(exit_room.h), LogManager.CATEGORY_DUNGEON)
return {} # No valid walls for stairs return {} # No valid walls for stairs
# Choose a random wall # Choose a random wall
@@ -1462,7 +1463,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
if wall.dir == "UP" or wall.dir == "DOWN": if wall.dir == "UP" or wall.dir == "DOWN":
# Horizontal stairs (3x2) # Horizontal stairs (3x2)
if wall.x_range.size() == 0: if wall.x_range.size() == 0:
print("DungeonGenerator: ERROR - x_range is empty for ", wall.dir, " stairs") LogManager.log_error("DungeonGenerator: ERROR - x_range is empty for " + str(wall.dir) + " stairs", LogManager.CATEGORY_DUNGEON)
return {} return {}
# Try to find a position that doesn't overlap doors # Try to find a position that doesn't overlap doors
@@ -1473,7 +1474,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
valid_positions.append(test_x) valid_positions.append(test_x)
if valid_positions.size() == 0: if valid_positions.size() == 0:
print("DungeonGenerator: ERROR - No valid position found for ", wall.dir, " stairs (all positions overlap doors)") LogManager.log_error("DungeonGenerator: ERROR - No valid position found for " + str(wall.dir) + " stairs (all positions overlap doors)", LogManager.CATEGORY_DUNGEON)
# Don't allow stairs to overlap doors - this is a critical bug # Don't allow stairs to overlap doors - this is a critical bug
# Instead, try the next wall or return empty to force placement elsewhere # Instead, try the next wall or return empty to force placement elsewhere
return {} # No valid position found - will trigger _force_place_stairs return {} # No valid position found - will trigger _force_place_stairs
@@ -1493,7 +1494,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
"world_size": Vector2(wall.w * tile_size, wall.h * tile_size) "world_size": Vector2(wall.w * tile_size, wall.h * tile_size)
} }
print("DungeonGenerator: Placed ", wall.dir, " stairs at tile (", stairs_start_x, ",", wall.y, ") world pos: ", stairs_data.world_pos, " in room (", exit_room.x, ",", exit_room.y, ") size ", exit_room.w, "x", exit_room.h) LogManager.log("DungeonGenerator: Placed " + str(wall.dir) + " stairs at tile (" + str(stairs_start_x) + "," + str(wall.y) + ") world pos: " + str(stairs_data.world_pos) + " in room (" + str(exit_room.x) + "," + str(exit_room.y) + ") size " + str(exit_room.w) + "x" + str(exit_room.h), LogManager.CATEGORY_DUNGEON)
# Mark grid cells as stairs (similar to doors) # Mark grid cells as stairs (similar to doors)
for dx in range(wall.w): for dx in range(wall.w):
@@ -1524,7 +1525,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
elif wall.dir == "LEFT" or wall.dir == "RIGHT": elif wall.dir == "LEFT" or wall.dir == "RIGHT":
# Vertical stairs (2x3) # Vertical stairs (2x3)
if wall.y_range.size() == 0: if wall.y_range.size() == 0:
print("DungeonGenerator: ERROR - y_range is empty for ", wall.dir, " stairs") LogManager.log_error("DungeonGenerator: ERROR - y_range is empty for " + str(wall.dir) + " stairs", LogManager.CATEGORY_DUNGEON)
return {} return {}
# Try to find a position that doesn't overlap doors # Try to find a position that doesn't overlap doors
@@ -1535,7 +1536,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
valid_positions.append(test_y) valid_positions.append(test_y)
if valid_positions.size() == 0: if valid_positions.size() == 0:
print("DungeonGenerator: ERROR - No valid position found for ", wall.dir, " stairs (all positions overlap doors)") LogManager.log_error("DungeonGenerator: ERROR - No valid position found for " + str(wall.dir) + " stairs (all positions overlap doors)", LogManager.CATEGORY_DUNGEON)
# Don't allow stairs to overlap doors - this is a critical bug # Don't allow stairs to overlap doors - this is a critical bug
# Instead, try the next wall or return empty to force placement elsewhere # Instead, try the next wall or return empty to force placement elsewhere
return {} # No valid position found - will trigger _force_place_stairs return {} # No valid position found - will trigger _force_place_stairs
@@ -1555,7 +1556,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
"world_size": Vector2(wall.w * tile_size, wall.h * tile_size) "world_size": Vector2(wall.w * tile_size, wall.h * tile_size)
} }
print("DungeonGenerator: Placed ", wall.dir, " stairs at tile (", wall.x, ",", stairs_start_y, ") world pos: ", stairs_data.world_pos, " in room (", exit_room.x, ",", exit_room.y, ") size ", exit_room.w, "x", exit_room.h) LogManager.log("DungeonGenerator: Placed " + str(wall.dir) + " stairs at tile (" + str(wall.x) + "," + str(stairs_start_y) + ") world pos: " + str(stairs_data.world_pos) + " in room (" + str(exit_room.x) + "," + str(exit_room.y) + ") size " + str(exit_room.w) + "x" + str(exit_room.h), LogManager.CATEGORY_DUNGEON)
# Mark grid cells as stairs # Mark grid cells as stairs
for dx in range(wall.w): for dx in range(wall.w):
@@ -1588,7 +1589,7 @@ func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, m
# Force place stairs in exit room - used as fallback when normal placement fails # Force place stairs in exit room - used as fallback when normal placement fails
# Still tries to avoid door overlaps, but will place stairs even if room is small # Still tries to avoid door overlaps, but will place stairs even if room is small
# Uses same positioning logic as doors: at least 2 tiles from corners # Uses same positioning logic as doors: at least 2 tiles from corners
print("DungeonGenerator: Force placing stairs in exit room: ", exit_room.x, ",", exit_room.y, " size: ", exit_room.w, "x", exit_room.h) LogManager.log("DungeonGenerator: Force placing stairs in exit room: " + str(exit_room.x) + "," + str(exit_room.y) + " size: " + str(exit_room.w) + "x" + str(exit_room.h), LogManager.CATEGORY_DUNGEON)
var stairs_data: Dictionary = {} var stairs_data: Dictionary = {}
var tile_size = 16 var tile_size = 16
@@ -1747,7 +1748,7 @@ func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, m
# If still no valid position found, return empty (don't place stairs that overlap doors!) # If still no valid position found, return empty (don't place stairs that overlap doors!)
if not found_position: if not found_position:
print("DungeonGenerator: ERROR - Could not find any position for stairs that doesn't overlap doors!") LogManager.log_error("DungeonGenerator: ERROR - Could not find any position for stairs that doesn't overlap doors!", LogManager.CATEGORY_DUNGEON)
return {} return {}
stairs_data = { stairs_data = {
@@ -1807,7 +1808,7 @@ func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, m
# Fallback: use UP stairs tiles # Fallback: use UP stairs tiles
tile_grid[x][y] = STAIRS_UP_START + Vector2i(dx, dy) tile_grid[x][y] = STAIRS_UP_START + Vector2i(dx, dy)
print("DungeonGenerator: Force placed ", stairs_dir, " stairs at tile (", stairs_data.x, ",", stairs_data.y, ") world pos: ", stairs_data.world_pos) LogManager.log("DungeonGenerator: Force placed " + str(stairs_dir) + " stairs at tile (" + str(stairs_data.x) + "," + str(stairs_data.y) + ") world pos: " + str(stairs_data.world_pos), LogManager.CATEGORY_DUNGEON)
return stairs_data return stairs_data
func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size: Vector2i, all_doors: Array, all_enemies: Array, rng: RandomNumberGenerator, room_puzzle_data: Dictionary = {}) -> Array: func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size: Vector2i, all_doors: Array, all_enemies: Array, rng: RandomNumberGenerator, room_puzzle_data: Dictionary = {}) -> Array:
@@ -1828,13 +1829,13 @@ func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size
if puzzle_room.x == room.x and puzzle_room.y == room.y and \ if puzzle_room.x == room.x and puzzle_room.y == room.y and \
puzzle_room.w == room.w and puzzle_room.h == room.h: puzzle_room.w == room.w and puzzle_room.h == room.h:
var puzzle_info = room_puzzle_data[puzzle_room] var puzzle_info = room_puzzle_data[puzzle_room]
print("DungeonGenerator: Checking room (", room.x, ",", room.y, ") - puzzle_room (", puzzle_room.x, ",", puzzle_room.y, ") puzzle_type: ", puzzle_info.type) LogManager.log("DungeonGenerator: Checking room (" + str(room.x) + "," + str(room.y) + ") - puzzle_room (" + str(puzzle_room.x) + "," + str(puzzle_room.y) + ") puzzle_type: " + str(puzzle_info.type), LogManager.CATEGORY_DUNGEON)
if puzzle_info.type == "switch_pillar": if puzzle_info.type == "switch_pillar":
has_pillar_switch_puzzle = true has_pillar_switch_puzzle = true
print("DungeonGenerator: Room (", room.x, ",", room.y, ") has pillar switch puzzle - will spawn at least 1 pillar") LogManager.log("DungeonGenerator: Room (" + str(room.x) + "," + str(room.y) + ") has pillar switch puzzle - will spawn at least 1 pillar", LogManager.CATEGORY_DUNGEON)
break break
else: else:
print("DungeonGenerator: room_puzzle_data is empty for room (", room.x, ",", room.y, ")") LogManager.log("DungeonGenerator: room_puzzle_data is empty for room (" + str(room.x) + "," + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
# Calculate room floor area (excluding walls) # Calculate room floor area (excluding walls)
var floor_w = room.w - 4 # Excluding 2-tile walls on each side var floor_w = room.w - 4 # Excluding 2-tile walls on each side
@@ -2090,7 +2091,7 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
# STEP 1: For each room (except start/exit), randomly decide if it has a door-puzzle # STEP 1: For each room (except start/exit), randomly decide if it has a door-puzzle
var puzzle_room_chance = 0.4 # 40% chance per room var puzzle_room_chance = 0.4 # 40% chance per room
print("DungeonGenerator: Assigning puzzles to rooms (", all_rooms.size(), " total rooms, excluding start/exit)") LogManager.log("DungeonGenerator: Assigning puzzles to rooms (" + str(all_rooms.size()) + " total rooms, excluding start/exit)", LogManager.CATEGORY_DUNGEON)
for i in range(all_rooms.size()): for i in range(all_rooms.size()):
if i == start_room_index or i == exit_room_index: if i == start_room_index or i == exit_room_index:
continue # Skip start and exit rooms continue # Skip start and exit rooms
@@ -2098,7 +2099,7 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
var room = all_rooms[i] var room = all_rooms[i]
if rng.randf() < puzzle_room_chance: if rng.randf() < puzzle_room_chance:
print("DungeonGenerator: Room (", room.x, ", ", room.y, ") selected for puzzle assignment") LogManager.log("DungeonGenerator: Room (" + str(room.x) + ", " + str(room.y) + ") selected for puzzle assignment", LogManager.CATEGORY_DUNGEON)
# This room has a puzzle! # This room has a puzzle!
# CRITICAL SAFETY CHECK: Never assign puzzles to start or exit rooms # CRITICAL SAFETY CHECK: Never assign puzzles to start or exit rooms
# Double-check even though we skip them in the loop # Double-check even though we skip them in the loop
@@ -2128,10 +2129,10 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
doors_in_room.append(door) doors_in_room.append(door)
if doors_in_room.size() == 0: if doors_in_room.size() == 0:
print("DungeonGenerator: Room (", room.x, ", ", room.y, ") has no doors connected - skipping puzzle assignment") LogManager.log("DungeonGenerator: Room (" + str(room.x) + ", " + str(room.y) + ") has no doors connected - skipping puzzle assignment", LogManager.CATEGORY_DUNGEON)
continue # No doors connected to this room, skip continue # No doors connected to this room, skip
print("DungeonGenerator: Room (", room.x, ", ", room.y, ") has ", doors_in_room.size(), " doors - assigning puzzle") LogManager.log("DungeonGenerator: Room (" + str(room.x) + ", " + str(room.y) + ") has " + str(doors_in_room.size()) + " doors - assigning puzzle", LogManager.CATEGORY_DUNGEON)
# Decide puzzle type: 33% walk switch, 33% pillar switch, 33% enemy spawner (if room is large enough) # Decide puzzle type: 33% walk switch, 33% pillar switch, 33% enemy spawner (if room is large enough)
var can_have_enemies = false var can_have_enemies = false
@@ -2153,13 +2154,13 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
"type": puzzle_type, "type": puzzle_type,
"doors": doors_in_room "doors": doors_in_room
} }
print("DungeonGenerator: Stored puzzle data for room (", room.x, ", ", room.y, ") - type: ", puzzle_type, ", doors: ", doors_in_room.size()) LogManager.log("DungeonGenerator: Stored puzzle data for room (" + str(room.x) + ", " + str(room.y) + ") - type: " + str(puzzle_type) + ", doors: " + str(doors_in_room.size()), LogManager.CATEGORY_DUNGEON)
# Mark these doors as assigned # Mark these doors as assigned
for door in doors_in_room: for door in doors_in_room:
assigned_doors.append(door) assigned_doors.append(door)
print("DungeonGenerator: Assigned puzzles to ", room_puzzle_data.size(), " rooms") LogManager.log("DungeonGenerator: Assigned puzzles to " + str(room_puzzle_data.size()) + " rooms", LogManager.CATEGORY_DUNGEON)
# STEP 2: Create blocking doors for rooms with puzzles # STEP 2: Create blocking doors for rooms with puzzles
# CRITICAL: Blocking doors should ONLY be placed ON THE DOORS IN THE PUZZLE ROOM # CRITICAL: Blocking doors should ONLY be placed ON THE DOORS IN THE PUZZLE ROOM
@@ -2168,7 +2169,7 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
for room in room_puzzle_data.keys(): for room in room_puzzle_data.keys():
# CRITICAL SAFETY CHECK #1: Verify this room is actually in room_puzzle_data # CRITICAL SAFETY CHECK #1: Verify this room is actually in room_puzzle_data
if not room in room_puzzle_data: if not room in room_puzzle_data:
push_error("DungeonGenerator: ERROR - Room (", room.x, ", ", room.y, ") is NOT in room_puzzle_data! This should never happen!") LogManager.log_error("DungeonGenerator: ERROR - Room (" + str(room.x) + ", " + str(room.y) + ") is NOT in room_puzzle_data! This should never happen!", LogManager.CATEGORY_DUNGEON)
continue continue
# CRITICAL SAFETY CHECK #2: Never create blocking doors for start or exit rooms # CRITICAL SAFETY CHECK #2: Never create blocking doors for start or exit rooms
@@ -2181,12 +2182,12 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
break break
if room_index == start_room_index or room_index == exit_room_index: if room_index == start_room_index or room_index == exit_room_index:
push_error("DungeonGenerator: ERROR - Attempted to create blocking doors for start/exit room! Skipping.") LogManager.log_error("DungeonGenerator: ERROR - Attempted to create blocking doors for start/exit room! Skipping.", LogManager.CATEGORY_DUNGEON)
continue continue
# CRITICAL SAFETY CHECK #3: Verify this room is actually in all_rooms (sanity check) # CRITICAL SAFETY CHECK #3: Verify this room is actually in all_rooms (sanity check)
if room_index == -1: if room_index == -1:
push_error("DungeonGenerator: ERROR - Room (", room.x, ", ", room.y, ") not found in all_rooms! Skipping.") LogManager.log_error("DungeonGenerator: ERROR - Room (" + str(room.x) + ", " + str(room.y) + ") not found in all_rooms! Skipping.", LogManager.CATEGORY_DUNGEON)
continue continue
var puzzle_info = room_puzzle_data[room] var puzzle_info = room_puzzle_data[room]
@@ -2194,7 +2195,7 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
var puzzle_type = puzzle_info.type var puzzle_type = puzzle_info.type
if doors_in_room.size() == 0: if doors_in_room.size() == 0:
print("DungeonGenerator: WARNING - Room has puzzle but no doors! Skipping.") LogManager.log("DungeonGenerator: WARNING - Room has puzzle but no doors! Skipping.", LogManager.CATEGORY_DUNGEON)
continue continue
# Randomly choose door type: 50% StoneDoor, 50% GateDoor # Randomly choose door type: 50% StoneDoor, 50% GateDoor
@@ -2219,9 +2220,9 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
"switch_data": switch_data, "switch_data": switch_data,
"switch_room": room "switch_room": room
} }
print("DungeonGenerator: Created switch puzzle for room (", room.x, ", ", room.y, ") - type: ", switch_type) LogManager.log("DungeonGenerator: Created switch puzzle for room (" + str(room.x) + ", " + str(room.y) + ") - type: " + str(switch_type), LogManager.CATEGORY_DUNGEON)
else: else:
print("DungeonGenerator: WARNING - Could not place floor switch in puzzle room (", room.x, ", ", room.y, ")! Skipping puzzle.") LogManager.log("DungeonGenerator: WARNING - Could not place floor switch in puzzle room (" + str(room.x) + ", " + str(room.y) + ")! Skipping puzzle.", LogManager.CATEGORY_DUNGEON)
elif puzzle_type == "enemy": elif puzzle_type == "enemy":
# Add enemy spawner IN THE PUZZLE ROOM # Add enemy spawner IN THE PUZZLE ROOM
var spawner_positions = [] var spawner_positions = []
@@ -2246,13 +2247,13 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
"spawner_data": spawner_data, "spawner_data": spawner_data,
"spawner_room": room "spawner_room": room
} }
print("DungeonGenerator: Created enemy spawner puzzle for room (", room.x, ", ", room.y, ") - spawner at ", spawner_data.position) LogManager.log("DungeonGenerator: Created enemy spawner puzzle for room (" + str(room.x) + ", " + str(room.y) + ") - spawner at " + str(spawner_data.position), LogManager.CATEGORY_DUNGEON)
else: else:
print("DungeonGenerator: WARNING - Could not place enemy spawner in puzzle room (", room.x, ", ", room.y, ")! Skipping puzzle.") LogManager.log("DungeonGenerator: WARNING - Could not place enemy spawner in puzzle room (" + str(room.x) + ", " + str(room.y) + ")! Skipping puzzle.", LogManager.CATEGORY_DUNGEON)
# CRITICAL SAFETY CHECK: Only create blocking doors if puzzle element was successfully created # CRITICAL SAFETY CHECK: Only create blocking doors if puzzle element was successfully created
if not puzzle_element_created: if not puzzle_element_created:
push_error("DungeonGenerator: ERROR - Puzzle element was NOT created for room (", room.x, ", ", room.y, ") with puzzle_type: ", puzzle_type, "! Skipping ALL doors in this room.") LogManager.log_error("DungeonGenerator: ERROR - Puzzle element was NOT created for room (" + str(room.x) + ", " + str(room.y) + ") with puzzle_type: " + str(puzzle_type) + "! Skipping ALL doors in this room.", LogManager.CATEGORY_DUNGEON)
# Remove doors from assigned list since we're not creating the puzzle # Remove doors from assigned list since we're not creating the puzzle
for door in doors_in_room: for door in doors_in_room:
if door in assigned_doors: if door in assigned_doors:
@@ -2261,12 +2262,12 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
# CRITICAL: Verify puzzle_element_data is valid before proceeding # CRITICAL: Verify puzzle_element_data is valid before proceeding
if puzzle_element_data.is_empty() or not puzzle_element_data.has("type"): if puzzle_element_data.is_empty() or not puzzle_element_data.has("type"):
push_error("DungeonGenerator: ERROR - puzzle_element_data is invalid for room (", room.x, ", ", room.y, ")! puzzle_element_created was true but data is empty!") LogManager.log_error("DungeonGenerator: ERROR - puzzle_element_data is invalid for room (" + str(room.x) + ", " + str(room.y) + ")! puzzle_element_created was true but data is empty!", LogManager.CATEGORY_DUNGEON)
continue continue
# Create blocking doors for at least 1 door (minimum), or all doors in the room # Create blocking doors for at least 1 door (minimum), or all doors in the room
# For now, create blocking doors for ALL doors in the puzzle room # For now, create blocking doors for ALL doors in the puzzle room
print("DungeonGenerator: Creating blocking doors for room (", room.x, ", ", room.y, ") with ", doors_in_room.size(), " doors, puzzle type: ", puzzle_type, ", puzzle_element type: ", puzzle_element_data.type) LogManager.log("DungeonGenerator: Creating blocking doors for room (" + str(room.x) + ", " + str(room.y) + ") with " + str(doors_in_room.size()) + " doors, puzzle type: " + str(puzzle_type) + ", puzzle_element type: " + str(puzzle_element_data.type), LogManager.CATEGORY_DUNGEON)
for door in doors_in_room: for door in doors_in_room:
# Determine direction based on which WALL of the PUZZLE ROOM the door is on # Determine direction based on which WALL of the PUZZLE ROOM the door is on
var direction = _determine_door_direction_for_puzzle_room(door, room, all_rooms) var direction = _determine_door_direction_for_puzzle_room(door, room, all_rooms)
@@ -2343,7 +2344,7 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
# Position is the OPEN state position (will move to CLOSED when entering room) # Position is the OPEN state position (will move to CLOSED when entering room)
# CRITICAL: Verify room is still a valid puzzle room before creating door # CRITICAL: Verify room is still a valid puzzle room before creating door
if not room in room_puzzle_data: if not room in room_puzzle_data:
push_error("DungeonGenerator: ERROR - Room (", room.x, ", ", room.y, ") is no longer in room_puzzle_data! Cannot create door.") LogManager.log_error("DungeonGenerator: ERROR - Room (" + str(room.x) + ", " + str(room.y) + ") is no longer in room_puzzle_data! Cannot create door.", LogManager.CATEGORY_DUNGEON)
continue continue
# NOTE: door_room1 is already declared at line 1933 and verified to match puzzle room at line 1935-1940 # NOTE: door_room1 is already declared at line 1933 and verified to match puzzle room at line 1935-1940
@@ -2364,7 +2365,7 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
# Store puzzle room as room1 for blocking doors # Store puzzle room as room1 for blocking doors
door_data.original_room1 = room # Puzzle room is always room1 for blocking doors door_data.original_room1 = room # Puzzle room is always room1 for blocking doors
print("DungeonGenerator: Creating blocking door for puzzle room (", room.x, ", ", room.y, ") - direction: ", direction, ", open_tile: (", open_tile_x, ",", open_tile_y, ")") LogManager.log("DungeonGenerator: Creating blocking door for puzzle room (" + str(room.x) + ", " + str(room.y) + ") - direction: " + str(direction) + ", open_tile: (" + str(open_tile_x) + "," + str(open_tile_y) + ")", LogManager.CATEGORY_DUNGEON)
# CRITICAL: Add puzzle-specific data from the puzzle_element_data created above (shared across all doors in room) # CRITICAL: Add puzzle-specific data from the puzzle_element_data created above (shared across all doors in room)
# Only add door if puzzle element data is valid # Only add door if puzzle element data is valid
@@ -2379,7 +2380,7 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
door_data.switch_type = puzzle_element_data.switch_type door_data.switch_type = puzzle_element_data.switch_type
door_data.switch_required_weight = puzzle_element_data.switch_weight door_data.switch_required_weight = puzzle_element_data.switch_weight
door_has_valid_puzzle = true door_has_valid_puzzle = true
print("DungeonGenerator: Added switch data to door - switch at (", door_data.switch_tile_x, ", ", door_data.switch_tile_y, ")") LogManager.log("DungeonGenerator: Added switch data to door - switch at (" + str(door_data.switch_tile_x) + ", " + str(door_data.switch_tile_y) + ")", LogManager.CATEGORY_DUNGEON)
elif puzzle_element_data.has("type") and puzzle_element_data.type == "enemy": elif puzzle_element_data.has("type") and puzzle_element_data.type == "enemy":
if puzzle_element_data.has("spawner_data") and puzzle_element_data.spawner_data.has("position"): if puzzle_element_data.has("spawner_data") and puzzle_element_data.spawner_data.has("position"):
if not "enemy_spawners" in door_data: if not "enemy_spawners" in door_data:
@@ -2393,11 +2394,11 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
}) })
door_data.requires_enemies = true door_data.requires_enemies = true
door_has_valid_puzzle = true door_has_valid_puzzle = true
print("DungeonGenerator: Added enemy spawner data to door - spawner at (", puzzle_element_data.spawner_data.tile_x, ", ", puzzle_element_data.spawner_data.tile_y, ")") LogManager.log("DungeonGenerator: Added enemy spawner data to door - spawner at (" + str(puzzle_element_data.spawner_data.tile_x) + ", " + str(puzzle_element_data.spawner_data.tile_y) + ")", LogManager.CATEGORY_DUNGEON)
# CRITICAL SAFETY CHECK: Only add door to blocking doors if it has valid puzzle element # CRITICAL SAFETY CHECK: Only add door to blocking doors if it has valid puzzle element
if not door_has_valid_puzzle: if not door_has_valid_puzzle:
push_error("DungeonGenerator: ERROR - Blocking door for room (", room.x, ", ", room.y, ") has no valid puzzle element! Skipping door. puzzle_type: ", puzzle_type, ", puzzle_element_data: ", puzzle_element_data) LogManager.log_error("DungeonGenerator: ERROR - Blocking door for room (" + str(room.x) + ", " + str(room.y) + ") has no valid puzzle element! Skipping door. puzzle_type: " + str(puzzle_type) + ", puzzle_element_data: " + str(puzzle_element_data), LogManager.CATEGORY_DUNGEON)
continue # Skip this door - don't add it to blocking_doors continue # Skip this door - don't add it to blocking_doors
# FINAL SAFETY CHECK: Verify door has either requires_switch or requires_enemies set # FINAL SAFETY CHECK: Verify door has either requires_switch or requires_enemies set
@@ -2405,18 +2406,18 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
var has_switch = door_data.get("requires_switch", false) == true var has_switch = door_data.get("requires_switch", false) == true
var has_enemies = door_data.get("requires_enemies", false) == true var has_enemies = door_data.get("requires_enemies", false) == true
if not has_switch and not has_enemies: if not has_switch and not has_enemies:
push_error("DungeonGenerator: ERROR - Blocking door (StoneDoor/GateDoor) has neither requires_switch nor requires_enemies! Door data: ", door_data.keys(), " - SKIPPING DOOR") LogManager.log_error("DungeonGenerator: ERROR - Blocking door (StoneDoor/GateDoor) has neither requires_switch nor requires_enemies! Door data: " + str(door_data.keys()) + " - SKIPPING DOOR", LogManager.CATEGORY_DUNGEON)
continue # Skip this door - it's invalid continue # Skip this door - it's invalid
# FINAL CRITICAL SAFETY CHECK: Verify door's blocking_room matches the puzzle room exactly # FINAL CRITICAL SAFETY CHECK: Verify door's blocking_room matches the puzzle room exactly
if door_data.blocking_room.x != room.x or door_data.blocking_room.y != room.y or \ if door_data.blocking_room.x != room.x or door_data.blocking_room.y != room.y or \
door_data.blocking_room.w != room.w or door_data.blocking_room.h != room.h: door_data.blocking_room.w != room.w or door_data.blocking_room.h != room.h:
push_error("DungeonGenerator: ERROR - Door blocking_room (", door_data.blocking_room.x, ",", door_data.blocking_room.y, ") doesn't match puzzle room (", room.x, ",", room.y, ")! This door is for wrong room! SKIPPING DOOR") LogManager.log_error("DungeonGenerator: ERROR - Door blocking_room (" + str(door_data.blocking_room.x) + "," + str(door_data.blocking_room.y) + ") doesn't match puzzle room (" + str(room.x) + "," + str(room.y) + ")! This door is for wrong room! SKIPPING DOOR", LogManager.CATEGORY_DUNGEON)
continue # Skip this door - it's for the wrong room continue # Skip this door - it's for the wrong room
# Add door to blocking doors list ONLY if it has valid puzzle element # Add door to blocking doors list ONLY if it has valid puzzle element
blocking_doors.append(door_data) blocking_doors.append(door_data)
print("DungeonGenerator: Created blocking door for puzzle room (", room.x, ", ", room.y, ") - direction: ", direction, ", open tile: (", open_tile_x, ", ", open_tile_y, "), puzzle_type: ", puzzle_type, ", has_switch: ", door_data.get("requires_switch", false), ", has_enemies: ", door_data.get("requires_enemies", false)) LogManager.log("DungeonGenerator: Created blocking door for puzzle room (" + str(room.x) + ", " + str(room.y) + ") - direction: " + str(direction) + ", open tile: (" + str(open_tile_x) + ", " + str(open_tile_y) + "), puzzle_type: " + str(puzzle_type) + ", has_switch: " + str(door_data.get("requires_switch", false)) + ", has_enemies: " + str(door_data.get("requires_enemies", false)), LogManager.CATEGORY_DUNGEON)
# STEP 3: Randomly assign some doors as KeyDoors (except start/exit room doors and already assigned doors) # STEP 3: Randomly assign some doors as KeyDoors (except start/exit room doors and already assigned doors)
var key_door_chance = 0.2 # 20% chance per door var key_door_chance = 0.2 # 20% chance per door

View File

@@ -142,10 +142,9 @@ func _physics_process(delta):
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_enemy_position"): if game_world and game_world.has_method("_sync_enemy_position"):
# Send via game_world using enemy name/index and position for identification # Send via game_world using enemy name/index and position for identification
game_world._sync_enemy_position.rpc(enemy_name, enemy_index, position, velocity, position_z, current_direction, anim_frame, "", 0, state_val) game_world._rpc_to_ready_peers("_sync_enemy_position", [enemy_name, enemy_index, position, velocity, position_z, current_direction, anim_frame, "", 0, state_val])
else: # Removed fallback rpc() call - it causes node path resolution errors
# Fallback: try direct rpc (may fail if node path doesn't match) # If game_world is not available, skip sync (will sync next frame)
rpc("_sync_position", position, velocity, position_z, current_direction, anim_frame, "", 0, state_val)
func _ai_behavior(_delta): func _ai_behavior(_delta):
# Override in subclasses # Override in subclasses
@@ -235,7 +234,7 @@ func _attack_player(player):
# Fallback: broadcast if we can't get peer_id # Fallback: broadcast if we can't get peer_id
player.rpc_take_damage.rpc(damage, global_position) player.rpc_take_damage.rpc(damage, global_position)
attack_timer = attack_cooldown attack_timer = attack_cooldown
print(name, " attacked ", player.name, " (peer: ", player_peer_id, ", server: ", multiplayer.get_unique_id(), ")") LogManager.log(str(name) + " attacked " + str(player.name) + " (peer: " + str(player_peer_id) + ", server: " + str(multiplayer.get_unique_id()) + ")", LogManager.CATEGORY_ENEMY)
func _find_nearest_player() -> Node: func _find_nearest_player() -> Node:
var players = get_tree().get_nodes_in_group("player") var players = get_tree().get_nodes_in_group("player")
@@ -306,7 +305,8 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
var dodge_chance = character_stats.dodge_chance var dodge_chance = character_stats.dodge_chance
if dodge_roll < dodge_chance: if dodge_roll < dodge_chance:
_was_dodged = true _was_dodged = true
print(name, " DODGED the attack! (DEX: ", character_stats.baseStats.dex + character_stats.get_pass("dex"), ", dodge chance: ", dodge_chance * 100.0, "%)") var dex_total = character_stats.baseStats.dex + character_stats.get_pass("dex")
LogManager.log(str(name) + " DODGED the attack! (DEX: " + str(dex_total) + ", dodge chance: " + str(dodge_chance * 100.0) + "%)", LogManager.CATEGORY_ENEMY)
# Show "DODGED" text # Show "DODGED" text
_show_damage_number(0.0, from_position, false, false, true) # is_dodged = true _show_damage_number(0.0, from_position, false, false, true) # is_dodged = true
# Sync dodge visual to clients (pass damage_amount=0.0 and is_dodged=true to indicate dodge) # Sync dodge visual to clients (pass damage_amount=0.0 and is_dodged=true to indicate dodge)
@@ -315,7 +315,7 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1 var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_enemy_damage_visual"): if game_world and game_world.has_method("_sync_enemy_damage_visual"):
game_world._sync_enemy_damage_visual.rpc(enemy_name, enemy_index, 0.0, from_position, false, true) game_world._rpc_to_ready_peers("_sync_enemy_damage_visual", [enemy_name, enemy_index, 0.0, from_position, false, true])
return # No damage taken, exit early return # No damage taken, exit early
# If not dodged, apply damage with DEF reduction # If not dodged, apply damage with DEF reduction
@@ -328,12 +328,12 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
if character_stats.hp <= 0: if character_stats.hp <= 0:
character_stats.no_health.emit() character_stats.no_health.emit()
var effective_def = character_stats.defense * (0.2 if is_critical else 1.0) var effective_def = character_stats.defense * (0.2 if is_critical else 1.0)
print(name, " took ", actual_damage, " damage (", amount, " base - ", effective_def, " DEF = ", actual_damage, ")! Health: ", current_health, "/", character_stats.maxhp) LogManager.log(str(name) + " took " + str(actual_damage) + " damage (" + str(amount) + " base - " + str(effective_def) + " DEF = " + str(actual_damage) + ")! Health: " + str(current_health) + "/" + str(character_stats.maxhp), LogManager.CATEGORY_ENEMY)
else: else:
# Fallback for legacy (shouldn't happen if _initialize_character_stats is called) # Fallback for legacy (shouldn't happen if _initialize_character_stats is called)
current_health -= amount current_health -= amount
actual_damage = amount actual_damage = amount
print(name, " took ", amount, " damage! Health: ", current_health, " (critical: ", is_critical, ")") LogManager.log(str(name) + " took " + str(amount) + " damage! Health: " + str(current_health) + " (critical: " + str(is_critical) + ")", LogManager.CATEGORY_ENEMY)
# Calculate knockback direction (away from attacker) # Calculate knockback direction (away from attacker)
var knockback_direction = (global_position - from_position).normalized() var knockback_direction = (global_position - from_position).normalized()
@@ -357,7 +357,7 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1 var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_enemy_damage_visual"): if game_world and game_world.has_method("_sync_enemy_damage_visual"):
game_world._sync_enemy_damage_visual.rpc(enemy_name, enemy_index, actual_damage, from_position, is_critical) game_world._rpc_to_ready_peers("_sync_enemy_damage_visual", [enemy_name, enemy_index, actual_damage, from_position, is_critical])
else: else:
# Fallback: try direct RPC (may fail if node path doesn't match) # Fallback: try direct RPC (may fail if node path doesn't match)
_sync_damage_visual.rpc(actual_damage, from_position, is_critical) _sync_damage_visual.rpc(actual_damage, from_position, is_critical)
@@ -467,7 +467,7 @@ func _notify_doors_enemy_died():
if door.requires_enemies: if door.requires_enemies:
# Trigger puzzle state check immediately (doors will verify if all enemies are dead) # Trigger puzzle state check immediately (doors will verify if all enemies are dead)
door.call_deferred("_check_puzzle_state") door.call_deferred("_check_puzzle_state")
print("Enemy: Notified door ", door.name, " to check puzzle state after enemy death") LogManager.log("Enemy: Notified door " + str(door.name) + " to check puzzle state after enemy death", LogManager.CATEGORY_ENEMY)
func _set_animation(_anim_name: String): func _set_animation(_anim_name: String):
# Virtual function - override in subclasses that use animation state system # Virtual function - override in subclasses that use animation state system
@@ -479,17 +479,26 @@ func _die():
return return
is_dead = true is_dead = true
print(name, " died!") LogManager.log(str(name) + " died!", LogManager.CATEGORY_ENEMY)
# Track defeated enemy for syncing to new clients
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
if enemy_index >= 0:
game_world.defeated_enemies[enemy_index] = true
LogManager.log("Enemy: Tracked defeated enemy with index " + str(enemy_index), LogManager.CATEGORY_ENEMY)
# Credit kill and grant EXP to the player who dealt the fatal damage # Credit kill and grant EXP to the player who dealt the fatal damage
if killer_player and is_instance_valid(killer_player) and killer_player.character_stats: if killer_player and is_instance_valid(killer_player) and killer_player.character_stats:
killer_player.character_stats.kills += 1 killer_player.character_stats.kills += 1
print(name, " kill credited to ", killer_player.name, " (total kills: ", killer_player.character_stats.kills, ")") LogManager.log(str(name) + " kill credited to " + str(killer_player.name) + " (total kills: " + str(killer_player.character_stats.kills) + ")", LogManager.CATEGORY_ENEMY)
# Grant EXP to the killer # Grant EXP to the killer
if exp_reward > 0: if exp_reward > 0:
killer_player.character_stats.add_xp(exp_reward) killer_player.character_stats.add_xp(exp_reward)
print(name, " granted ", exp_reward, " EXP to ", killer_player.name) LogManager.log(str(name) + " granted " + str(exp_reward) + " EXP to " + str(killer_player.name), LogManager.CATEGORY_ENEMY)
# Sync kill update to client if this player belongs to a client # Sync kill update to client if this player belongs to a client
# Only sync if we're on the server and the killer is a client's player # Only sync if we're on the server and the killer is a client's player
@@ -499,7 +508,7 @@ func _die():
if killer_peer_id != 0 and killer_peer_id != multiplayer.get_unique_id() and killer_player.has_method("_sync_stats_update"): if killer_peer_id != 0 and killer_peer_id != multiplayer.get_unique_id() and killer_player.has_method("_sync_stats_update"):
# Server is updating a client's player stats - sync to the client # Server is updating a client's player stats - sync to the client
var coins = killer_player.character_stats.coin if "coin" in killer_player.character_stats else 0 var coins = killer_player.character_stats.coin if "coin" in killer_player.character_stats else 0
print(name, " syncing kill stats to client peer_id=", killer_peer_id, " kills=", killer_player.character_stats.kills, " coins=", coins) LogManager.log(str(name) + " syncing kill stats to client peer_id=" + str(killer_peer_id) + " kills=" + str(killer_player.character_stats.kills) + " coins=" + str(coins), LogManager.CATEGORY_ENEMY)
killer_player._sync_stats_update.rpc_id(killer_peer_id, killer_player.character_stats.kills, coins) killer_player._sync_stats_update.rpc_id(killer_peer_id, killer_player.character_stats.kills, coins)
# Spawn loot immediately (before death animation) # Spawn loot immediately (before death animation)
@@ -512,7 +521,7 @@ func _die():
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1 var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_enemy_death"): if game_world and game_world.has_method("_sync_enemy_death"):
game_world._sync_enemy_death.rpc(enemy_name, enemy_index) game_world._rpc_to_ready_peers("_sync_enemy_death", [enemy_name, enemy_index])
else: else:
# Fallback: try direct RPC (may fail if node path doesn't match) # Fallback: try direct RPC (may fail if node path doesn't match)
_sync_death.rpc() _sync_death.rpc()
@@ -528,20 +537,20 @@ func _play_death_animation():
func _spawn_loot(): func _spawn_loot():
# Only spawn loot on server/authority # Only spawn loot on server/authority
if not is_multiplayer_authority(): if not is_multiplayer_authority():
print(name, " _spawn_loot() called but not authority, skipping") LogManager.log(str(name) + " _spawn_loot() called but not authority, skipping", LogManager.CATEGORY_ENEMY)
return return
print(name, " _spawn_loot() called on authority") LogManager.log(str(name) + " _spawn_loot() called on authority", LogManager.CATEGORY_ENEMY)
# Spawn random loot at enemy position # Spawn random loot at enemy position
var loot_scene = preload("res://scenes/loot.tscn") var loot_scene = preload("res://scenes/loot.tscn")
if not loot_scene: if not loot_scene:
print(name, " ERROR: loot_scene is null!") LogManager.log_error(str(name) + " ERROR: loot_scene is null!", LogManager.CATEGORY_ENEMY)
return return
# Random chance to drop loot (70% chance) # Random chance to drop loot (70% chance)
var loot_chance = randf() var loot_chance = randf()
print(name, " loot chance roll: ", loot_chance, " (need > 0.3)") LogManager.log(str(name) + " loot chance roll: " + str(loot_chance) + " (need > 0.3)", LogManager.CATEGORY_ENEMY)
if loot_chance > 0.3: if loot_chance > 0.3:
# Decide what to drop: 30% coin, 30% food, 40% item # Decide what to drop: 30% coin, 30% food, 40% item
var drop_roll = randf() var drop_roll = randf()
@@ -575,7 +584,7 @@ func _spawn_loot():
var entities_node = get_parent() var entities_node = get_parent()
if not entities_node: if not entities_node:
print(name, " ERROR: entities_node is null! Cannot spawn loot!") LogManager.log_error(str(name) + " ERROR: entities_node is null! Cannot spawn loot!", LogManager.CATEGORY_ENEMY)
return return
if drop_item: if drop_item:
@@ -583,7 +592,7 @@ func _spawn_loot():
var item = ItemDatabase.get_random_enemy_drop() var item = ItemDatabase.get_random_enemy_drop()
if item: if item:
ItemLootHelper.spawn_item_loot(item, global_position, entities_node, game_world) ItemLootHelper.spawn_item_loot(item, global_position, entities_node, game_world)
print(name, " ✓ dropped item: ", item.item_name, " at ", safe_spawn_pos) LogManager.log(str(name) + " ✓ dropped item: " + str(item.item_name) + " at " + str(safe_spawn_pos), LogManager.CATEGORY_ENEMY)
else: else:
# Spawn regular loot (coin or food) # Spawn regular loot (coin or food)
var loot = loot_scene.instantiate() var loot = loot_scene.instantiate()
@@ -595,7 +604,7 @@ func _spawn_loot():
loot.velocity_z = random_velocity_z loot.velocity_z = random_velocity_z
loot.velocity_set_by_spawner = true loot.velocity_set_by_spawner = true
loot.is_airborne = true loot.is_airborne = true
print(name, " ✓ dropped loot: ", loot_type, " at ", safe_spawn_pos, " (original enemy pos: ", global_position, ")") LogManager.log(str(name) + " ✓ dropped loot: " + str(loot_type) + " at " + str(safe_spawn_pos) + " (original enemy pos: " + str(global_position) + ")", LogManager.CATEGORY_ENEMY)
# Sync loot spawn to all clients (use safe position) # Sync loot spawn to all clients (use safe position)
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
@@ -608,12 +617,12 @@ func _spawn_loot():
# Store loot ID on server loot instance # Store loot ID on server loot instance
loot.set_meta("loot_id", loot_id) loot.set_meta("loot_id", loot_id)
# Sync to clients with ID # Sync to clients with ID
game_world._sync_loot_spawn.rpc(safe_spawn_pos, loot_type, initial_velocity, random_velocity_z, loot_id) game_world._rpc_to_ready_peers("_sync_loot_spawn", [safe_spawn_pos, loot_type, initial_velocity, random_velocity_z, loot_id])
print(name, " ✓ synced loot spawn to clients") LogManager.log(str(name) + " ✓ synced loot spawn to clients", LogManager.CATEGORY_ENEMY)
else: else:
print(name, " ERROR: game_world not found for loot sync!") LogManager.log_error(str(name) + " ERROR: game_world not found for loot sync!", LogManager.CATEGORY_ENEMY)
else: else:
print(name, " loot chance failed (", loot_chance, " <= 0.3), no loot dropped") LogManager.log(str(name) + " loot chance failed (" + str(loot_chance) + " <= 0.3), no loot dropped", LogManager.CATEGORY_ENEMY)
# This function can be called directly (not just via RPC) when game_world routes the update # This function can be called directly (not just via RPC) when game_world routes the update
func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0, frame: int = 0, anim: String = "", frame_num: int = 0, state_value: int = -1): func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0, frame: int = 0, anim: String = "", frame_num: int = 0, state_value: int = -1):
@@ -625,12 +634,12 @@ func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0
# Debug: Log when client receives position update (first few times) # Debug: Log when client receives position update (first few times)
if not has_meta("position_sync_count"): if not has_meta("position_sync_count"):
set_meta("position_sync_count", 0) set_meta("position_sync_count", 0)
print("Enemy ", name, " (client) RECEIVED first position sync! pos=", pos, " authority: ", get_multiplayer_authority(), " is_authority: ", is_multiplayer_authority(), " in_tree: ", is_inside_tree()) LogManager.log("Enemy " + str(name) + " (client) RECEIVED first position sync! pos=" + str(pos) + " authority: " + str(get_multiplayer_authority()) + " is_authority: " + str(is_multiplayer_authority()) + " in_tree: " + str(is_inside_tree()), LogManager.CATEGORY_ENEMY)
var sync_count = get_meta("position_sync_count") + 1 var sync_count = get_meta("position_sync_count") + 1
set_meta("position_sync_count", sync_count) set_meta("position_sync_count", sync_count)
if sync_count <= 3: # Log first 3 syncs if sync_count <= 3: # Log first 3 syncs
print("Enemy ", name, " (client) received position sync #", sync_count, ": pos=", pos) LogManager.log("Enemy " + str(name) + " (client) received position sync #" + str(sync_count) + ": pos=" + str(pos), LogManager.CATEGORY_ENEMY)
# Update position and state # Update position and state
position = pos position = pos
@@ -661,6 +670,10 @@ func _sync_damage_visual(damage_amount: float = 0.0, attacker_position: Vector2
if is_multiplayer_authority(): if is_multiplayer_authority():
return # Server ignores its own updates return # Server ignores its own updates
# Trigger damage animation and state change on client
# This ensures clients play the damage animation (e.g., slime DAMAGE animation)
_on_take_damage()
_flash_damage() _flash_damage()
# Show damage number on client (even if damage_amount is 0 for dodges/misses) # Show damage number on client (even if damage_amount is 0 for dodges/misses)
@@ -676,7 +689,7 @@ func _sync_death():
if not is_dead: if not is_dead:
is_dead = true is_dead = true
print(name, " received death sync, dying on client") LogManager.log(str(name) + " received death sync, dying on client", LogManager.CATEGORY_ENEMY)
# Remove collision layer so they don't collide with players, but still collide with walls # Remove collision layer so they don't collide with players, but still collide with walls
# This matches what happens on the server when rats/slimes die # This matches what happens on the server when rats/slimes die
@@ -687,7 +700,7 @@ func _sync_death():
_play_death_animation() _play_death_animation()
else: else:
# Already dead, but make sure collision is removed and it's removed from scene # Already dead, but make sure collision is removed and it's removed from scene
print(name, " received death sync but already dead, ensuring removal") LogManager.log(str(name) + " received death sync but already dead, ensuring removal", LogManager.CATEGORY_ENEMY)
# Remove collision layer if not already removed # Remove collision layer if not already removed
if get_collision_layer_value(2): if get_collision_layer_value(2):

View File

@@ -200,11 +200,11 @@ func _ready():
# But still deterministic across clients by using a synced random value # But still deterministic across clients by using a synced random value
var random_component = randi() # This will be different each spawn var random_component = randi() # This will be different each spawn
seed_value = hash(str(spawn_position) + str(humanoid_type) + str(random_component)) seed_value = hash(str(spawn_position) + str(humanoid_type) + str(random_component))
print(name, " appearance seed (randomized): ", seed_value, " at spawn position: ", spawn_position, " type: ", humanoid_type) LogManager.log(str(name) + " appearance seed (randomized): " + str(seed_value) + " at spawn position: " + str(spawn_position) + " type: " + str(humanoid_type), LogManager.CATEGORY_ENEMY)
else: else:
# Deterministic based on position and type only # Deterministic based on position and type only
seed_value = hash(str(spawn_position) + str(humanoid_type)) seed_value = hash(str(spawn_position) + str(humanoid_type))
print(name, " appearance seed (deterministic): ", seed_value, " at spawn position: ", spawn_position, " type: ", humanoid_type) LogManager.log(str(name) + " appearance seed (deterministic): " + str(seed_value) + " at spawn position: " + str(spawn_position) + " type: " + str(humanoid_type), LogManager.CATEGORY_ENEMY)
appearance_rng.seed = seed_value appearance_rng.seed = seed_value
# Set up appearance based on type # Set up appearance based on type
@@ -678,7 +678,7 @@ func _load_type_addons():
sprite_addons.texture = texture sprite_addons.texture = texture
sprite_addons.hframes = 35 sprite_addons.hframes = 35
sprite_addons.vframes = 8 sprite_addons.vframes = 8
print(name, " loaded type addon: ", addon_path) LogManager.log(str(name) + " loaded type addon: " + str(addon_path), LogManager.CATEGORY_ENEMY)
HumanoidType.SKELETON: HumanoidType.SKELETON:
# Can have (but not must) skeleton horns # Can have (but not must) skeleton horns
@@ -690,7 +690,7 @@ func _load_type_addons():
sprite_addons.texture = texture sprite_addons.texture = texture
sprite_addons.hframes = 35 sprite_addons.hframes = 35
sprite_addons.vframes = 8 sprite_addons.vframes = 8
print(name, " loaded skeleton horns: ", addon_path) LogManager.log(str(name) + " loaded skeleton horns: " + str(addon_path), LogManager.CATEGORY_ENEMY)
HumanoidType.HUMANOID: HumanoidType.HUMANOID:
# Ears are already set by _randomize_appearance, so skip here # Ears are already set by _randomize_appearance, so skip here
@@ -706,7 +706,7 @@ func _load_type_addons():
sprite_addons.texture = texture sprite_addons.texture = texture
sprite_addons.hframes = 35 sprite_addons.hframes = 35
sprite_addons.vframes = 8 sprite_addons.vframes = 8
print(name, " loaded night elf ears") LogManager.log(str(name) + " loaded night elf ears", LogManager.CATEGORY_ENEMY)
HumanoidType.DEMON: HumanoidType.DEMON:
# Can have DemonEars or DemonJaw # Can have DemonEars or DemonJaw
@@ -718,7 +718,7 @@ func _load_type_addons():
sprite_addons.texture = texture sprite_addons.texture = texture
sprite_addons.hframes = 35 sprite_addons.hframes = 35
sprite_addons.vframes = 8 sprite_addons.vframes = 8
print(name, " loaded demon addon: ", addon_path) LogManager.log(str(name) + " loaded demon addon: " + str(addon_path), LogManager.CATEGORY_ENEMY)
func _load_beastkin_addon(): func _load_beastkin_addon():
# Load random beastkin addon (low chance, can override type addon) # Load random beastkin addon (low chance, can override type addon)
@@ -734,7 +734,7 @@ func _load_beastkin_addon():
sprite_addons.texture = texture sprite_addons.texture = texture
sprite_addons.hframes = 35 sprite_addons.hframes = 35
sprite_addons.vframes = 8 sprite_addons.vframes = 8
print(name, " loaded beastkin addon: ", addon_path) LogManager.log(str(name) + " loaded beastkin addon: " + str(addon_path), LogManager.CATEGORY_ENEMY)
func _setup_stats(): func _setup_stats():
# Set stats based on type # Set stats based on type
@@ -796,7 +796,7 @@ func _setup_stats():
charge_multiplier = clamp(charge_multiplier, 0.375, 1.0) # Clamp between 0.375x (2.67x faster) and 1.0x charge_multiplier = clamp(charge_multiplier, 0.375, 1.0) # Clamp between 0.375x (2.67x faster) and 1.0x
base_attack_charge_time = 0.4 * charge_multiplier base_attack_charge_time = 0.4 * charge_multiplier
print(name, " stats: DEX=", dex, " attack_cooldown=", attack_cooldown, " charge_time=", base_attack_charge_time) LogManager.log(str(name) + " stats: DEX=" + str(dex) + " attack_cooldown=" + str(attack_cooldown) + " charge_time=" + str(base_attack_charge_time), LogManager.CATEGORY_ENEMY)
# Setup alert indicators # Setup alert indicators
if alert_indicator: if alert_indicator:
@@ -874,7 +874,7 @@ func _physics_process(delta):
# Send via game_world using enemy name/index and position for identification # Send via game_world using enemy name/index and position for identification
var enemy_name = name var enemy_name = name
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1 var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
game_world._sync_enemy_position.rpc(enemy_name, enemy_index, position, velocity, position_z, current_direction, 0, current_animation, current_frame, -1) game_world._rpc_to_ready_peers("_sync_enemy_position", [enemy_name, enemy_index, position, velocity, position_z, current_direction, 0, current_animation, current_frame, -1])
else: else:
# Fallback: try direct call to _sync_position (not RPC) # Fallback: try direct call to _sync_position (not RPC)
_sync_position(position, velocity, position_z, current_direction, 0, current_animation, current_frame) _sync_position(position, velocity, position_z, current_direction, 0, current_animation, current_frame)
@@ -1188,7 +1188,13 @@ func _perform_attack():
# Sync attack animation to clients first # Sync attack animation to clients first
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
_sync_attack.rpc(current_direction, attack_direction) # Use game_world to send RPC instead of rpc() on node instance
# This avoids node path resolution issues when clients haven't spawned yet
var game_world = get_tree().get_first_node_in_group("game_world")
var enemy_name = name
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
if game_world and game_world.has_method("_sync_enemy_attack"):
game_world._rpc_to_ready_peers("_sync_enemy_attack", [enemy_name, enemy_index, current_direction, attack_direction])
# Delay before spawning sword slash # Delay before spawning sword slash
await get_tree().create_timer(0.15).timeout await get_tree().create_timer(0.15).timeout
@@ -1207,7 +1213,7 @@ func _perform_attack():
if parent: if parent:
parent.add_child(projectile) parent.add_child(projectile)
else: else:
push_error("EnemyHumanoid: ERROR - No parent node to add projectile to!") LogManager.log_error("EnemyHumanoid: ERROR - No parent node to add projectile to!", LogManager.CATEGORY_ENEMY)
projectile.queue_free() projectile.queue_free()
func _try_attack_object(obj: Node): func _try_attack_object(obj: Node):
@@ -1231,11 +1237,11 @@ func _try_attack_object(obj: Node):
# Perform attack - sword projectile will damage the object if it hits # Perform attack - sword projectile will damage the object if it hits
# The object will handle damage from sword projectiles (sword_projectile.gd already handles this) # The object will handle damage from sword projectiles (sword_projectile.gd already handles this)
_perform_attack() _perform_attack()
print(name, " is attacking object ", obj.name, "!") LogManager.log(str(name) + " is attacking object " + str(obj.name) + "!", LogManager.CATEGORY_ENEMY)
@rpc("authority", "reliable")
func _sync_attack(direction: int, attack_dir: Vector2): func _sync_attack(direction: int, attack_dir: Vector2):
# Sync attack to clients # Sync attack to clients (called by game_world, not via RPC)
# Only process on clients (not authority)
if not is_multiplayer_authority(): if not is_multiplayer_authority():
current_direction = direction as Direction current_direction = direction as Direction
_set_animation("SWORD") _set_animation("SWORD")
@@ -1248,7 +1254,7 @@ func _sync_attack(direction: int, attack_dir: Vector2):
projectile.setup(attack_dir, self) projectile.setup(attack_dir, self)
var spawn_offset = attack_dir * 10.0 var spawn_offset = attack_dir * 10.0
projectile.global_position = global_position + spawn_offset projectile.global_position = global_position + spawn_offset
print(name, " performed synced attack!") LogManager.log(str(name) + " performed synced attack!", LogManager.CATEGORY_ENEMY)
func _set_animation(anim_name: String): func _set_animation(anim_name: String):
if anim_name in ANIMATIONS: if anim_name in ANIMATIONS:

View File

@@ -268,6 +268,19 @@ func _update_client_visuals():
# Update visuals on clients based on synced state # Update visuals on clients based on synced state
super._update_client_visuals() super._update_client_visuals()
# Map synced state to animation (similar to how bat/rat use state directly)
match state:
SlimeState.IDLE:
_set_animation("IDLE")
SlimeState.MOVING:
_set_animation("MOVE")
SlimeState.JUMPING:
_set_animation("JUMP")
SlimeState.DAMAGED:
_set_animation("DAMAGE")
SlimeState.DYING:
_set_animation("DIE")
# Update animation based on synced state # Update animation based on synced state
_update_animation(0.0) # Update animation immediately when state changes _update_animation(0.0) # Update animation immediately when state changes

View File

@@ -126,7 +126,7 @@ func spawn_enemy():
node = node.get_parent() node = node.get_parent()
if game_world and game_world.has_method("_sync_smoke_puffs"): if game_world and game_world.has_method("_sync_smoke_puffs"):
game_world._sync_smoke_puffs.rpc(name, puff_positions) game_world._rpc_to_ready_peers("_sync_smoke_puffs", [name, puff_positions])
# Wait for smoke puffs to finish animating before spawning enemy # Wait for smoke puffs to finish animating before spawning enemy
# Reduced duration for faster spawning: 4 frames * (1/10.0) seconds per frame = 0.4s, plus move_duration 1.0s, plus fade_duration 0.3s = ~1.7s total # Reduced duration for faster spawning: 4 frames * (1/10.0) seconds per frame = 0.4s, plus move_duration 1.0s, plus fade_duration 0.3s = ~1.7s total
@@ -239,7 +239,7 @@ func spawn_enemy():
# Pass humanoid_type if it's a humanoid enemy (for syncing to clients) # Pass humanoid_type if it's a humanoid enemy (for syncing to clients)
var sync_humanoid_type = humanoid_type if humanoid_type != null else -1 var sync_humanoid_type = humanoid_type if humanoid_type != null else -1
print(" DEBUG: Calling _sync_enemy_spawn.rpc with name=", name, " pos=", global_position, " scene_index=", scene_index, " humanoid_type=", sync_humanoid_type) print(" DEBUG: Calling _sync_enemy_spawn.rpc with name=", name, " pos=", global_position, " scene_index=", scene_index, " humanoid_type=", sync_humanoid_type)
game_world._sync_enemy_spawn.rpc(name, global_position, scene_index, sync_humanoid_type) game_world._rpc_to_ready_peers("_sync_enemy_spawn", [name, global_position, scene_index, sync_humanoid_type])
print(" Sent RPC to sync enemy spawn to clients: spawner=", name, " pos=", global_position, " scene_index=", scene_index, " humanoid_type=", sync_humanoid_type) print(" Sent RPC to sync enemy spawn to clients: spawner=", name, " pos=", global_position, " scene_index=", scene_index, " humanoid_type=", sync_humanoid_type)
else: else:
var has_method_str = str(game_world.has_method("_sync_enemy_spawn")) if game_world else "N/A" var has_method_str = str(game_world.has_method("_sync_enemy_spawn")) if game_world else "N/A"

View File

@@ -186,6 +186,13 @@ func _check_activation():
is_activated = should_activate is_activated = should_activate
_update_visual() _update_visual()
# Track activated switch for syncing to new clients
if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and is_activated:
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
game_world.activated_switches[name] = true
LogManager.log("FloorSwitch: Tracked activated switch " + name, LogManager.CATEGORY_NETWORK)
# Notify connected doors # Notify connected doors
_notify_doors() _notify_doors()

View File

@@ -9,18 +9,33 @@ extends CanvasLayer
@onready var network_mode_container = $Control/MainMenu/VBoxContainer/NetworkModeContainer @onready var network_mode_container = $Control/MainMenu/VBoxContainer/NetworkModeContainer
@onready var local_players_spinbox = $Control/MainMenu/VBoxContainer/LocalPlayersContainer/SpinBox @onready var local_players_spinbox = $Control/MainMenu/VBoxContainer/LocalPlayersContainer/SpinBox
@onready var address_input = $Control/MainMenu/VBoxContainer/AddressContainer/AddressInput @onready var address_input = $Control/MainMenu/VBoxContainer/AddressContainer/AddressInput
@onready var room_fetch_status_container = $Control/MainMenu/VBoxContainer/RoomFetchStatusContainer
@onready var loading_label = $Control/MainMenu/VBoxContainer/RoomFetchStatusContainer/RoomFetchLoadingContainer/LoadingLabel
@onready var loading_spinner = $Control/MainMenu/VBoxContainer/RoomFetchStatusContainer/RoomFetchLoadingContainer/LoadingSpinner
@onready var last_fetch_label = $Control/MainMenu/VBoxContainer/RoomFetchStatusContainer/LastFetchLabel
@onready var network_manager = $"/root/NetworkManager" @onready var network_manager = $"/root/NetworkManager"
var connection_error_label: Label = null
var connection_error_shown: bool = false # Prevent spamming error messages
var is_joining_attempt: bool = false
var last_join_address: String = ""
var room_fetch_timer: Timer = null # Timer for retrying room fetches
var is_auto_joining: bool = false # Track if we're in auto-join mode
var is_hosting: bool = false # Track if we're hosting (don't fetch rooms when hosting)
var room_list_container: VBoxContainer = null # Container for displaying available rooms
var refresh_button: Button = null # Refresh button for manually reloading rooms
var refresh_cooldown_timer: Timer = null # Timer for refresh button cooldown
func _ready(): func _ready():
# Wait for nodes to be ready # Wait for nodes to be ready
await get_tree().process_frame await get_tree().process_frame
# Debug: Print node paths # Debug: Print node paths (UI category - disabled by default for network debugging)
print("GameUI _ready() called") LogManager.log("GameUI _ready() called", LogManager.CATEGORY_UI)
print("Main menu node: ", main_menu) LogManager.log("Main menu node: " + str(main_menu), LogManager.CATEGORY_UI)
print("Host button node: ", host_button) LogManager.log("Host button node: " + str(host_button), LogManager.CATEGORY_UI)
print("Join button node: ", join_button) LogManager.log("Join button node: " + str(join_button), LogManager.CATEGORY_UI)
# Verify nodes exist # Verify nodes exist
if not host_button: if not host_button:
@@ -40,7 +55,7 @@ func _ready():
# On web builds, filter out ENet option (only WebRTC and WebSocket available) # On web builds, filter out ENet option (only WebRTC and WebSocket available)
if OS.get_name() == "Web": if OS.get_name() == "Web":
print("GameUI: Web platform detected, filtering network mode options") LogManager.log("GameUI: Web platform detected, filtering network mode options", LogManager.CATEGORY_UI)
# Remove ENet option (index 0) for web builds # Remove ENet option (index 0) for web builds
network_mode_option.remove_item(0) network_mode_option.remove_item(0)
# Adjust selected index (was 0 for ENet, now 0 is WebRTC) # Adjust selected index (was 0 for ENet, now 0 is WebRTC)
@@ -59,43 +74,52 @@ func _ready():
if network_manager: if network_manager:
network_manager.connection_succeeded.connect(_on_connection_succeeded) network_manager.connection_succeeded.connect(_on_connection_succeeded)
network_manager.connection_failed.connect(_on_connection_failed) network_manager.connection_failed.connect(_on_connection_failed)
# Connect to rooms_fetched for displaying rooms (non-auto-join mode)
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display):
network_manager.rooms_fetched.connect(_on_rooms_fetched_display)
else: else:
push_error("NetworkManager not found!") push_error("NetworkManager not found!")
# Check for command-line arguments # Check for command-line arguments
_check_command_line_args() _check_command_line_args()
# If WebRTC is selected at startup (not auto-joining and not hosting), fetch rooms
if not is_auto_joining and not is_hosting:
var current_mode = network_manager.network_mode
if current_mode == 1: # WebRTC
_start_room_fetch()
func _check_command_line_args(): func _check_command_line_args():
var args = OS.get_cmdline_args() var args = OS.get_cmdline_args()
print("GameUI: Parsing command-line arguments: ", args) LogManager.log("GameUI: Parsing command-line arguments: " + str(args), LogManager.CATEGORY_UI)
# Parse arguments # Parse arguments
var should_host = false var should_host = false
var should_join = false var should_join = false
var should_debug = false var should_debug = false
var force_webrtc = false var force_webrtc = false
var join_address = "ruinborn.thefirstboss.com" var join_address = "" # Empty by default - will fetch rooms if WebRTC/WebSocket
var local_count = 1 var local_count = 1
for arg in args: for arg in args:
if arg == "--host": if arg == "--host":
should_host = true should_host = true
print("GameUI: Found --host argument") LogManager.log("GameUI: Found --host argument", LogManager.CATEGORY_UI)
elif arg == "--join": elif arg == "--join":
should_join = true should_join = true
print("GameUI: Found --join argument") LogManager.log("GameUI: Found --join argument", LogManager.CATEGORY_UI)
elif arg == "--websocket" or arg == "--webrtc": elif arg == "--websocket" or arg == "--webrtc":
force_webrtc = true force_webrtc = true
print("GameUI: Found --websocket/--webrtc argument (forcing WebSocket mode)") LogManager.log("GameUI: Found --websocket/--webrtc argument (forcing WebSocket mode)", LogManager.CATEGORY_UI)
elif arg == "--room-debug": elif arg == "--room-debug":
should_debug = true should_debug = true
print("GameUI: Found --room-debug argument") LogManager.log("GameUI: Found --room-debug argument", LogManager.CATEGORY_UI)
elif arg.begins_with("--address="): elif arg.begins_with("--address="):
join_address = arg.split("=")[1] join_address = arg.split("=")[1]
elif arg.begins_with("--players="): elif arg.begins_with("--players="):
local_count = int(arg.split("=")[1]) local_count = int(arg.split("=")[1])
print("GameUI: Parsed flags - should_host: ", should_host, ", should_join: ", should_join, ", force_webrtc: ", force_webrtc, ", should_debug: ", should_debug) LogManager.log("GameUI: Parsed flags - should_host: " + str(should_host) + ", should_join: " + str(should_join) + ", force_webrtc: " + str(force_webrtc) + ", should_debug: " + str(should_debug), LogManager.CATEGORY_UI)
# Force WebRTC mode if --webrtc flag is present # Force WebRTC mode if --webrtc flag is present
if force_webrtc: if force_webrtc:
@@ -106,28 +130,368 @@ func _check_command_line_args():
else: else:
network_mode_option.selected = 1 # WebRTC is second option on native network_mode_option.selected = 1 # WebRTC is second option on native
_on_network_mode_changed(network_mode_option.selected) _on_network_mode_changed(network_mode_option.selected)
print("GameUI: WebRTC mode forced via --webrtc flag") LogManager.log("GameUI: WebRTC mode forced via --webrtc flag", LogManager.CATEGORY_UI)
# Set debug flag only if --room-debug is used with --host or --join # Set debug flag only if --room-debug is used with --host or --join
if should_debug and (should_host or should_join): if should_debug and (should_host or should_join):
network_manager.show_room_labels = true network_manager.show_room_labels = true
print("Debug mode enabled: room labels will be shown") LogManager.log("Debug mode enabled: room labels will be shown", LogManager.CATEGORY_UI)
else: else:
print("GameUI: Debug mode NOT enabled - should_debug: ", should_debug, ", should_host: ", should_host, ", should_join: ", should_join) LogManager.log("GameUI: Debug mode NOT enabled - should_debug: " + str(should_debug) + ", should_host: " + str(should_host) + ", should_join: " + str(should_join), LogManager.CATEGORY_UI)
# Auto-start based on arguments # Auto-start based on arguments
if should_host: if should_host:
print("Auto-hosting due to --host argument") is_hosting = true # Set flag so we don't fetch rooms
LogManager.log("Auto-hosting due to --host argument", LogManager.CATEGORY_UI)
network_manager.set_local_player_count(local_count) network_manager.set_local_player_count(local_count)
if network_manager.host_game(): if network_manager.host_game():
_start_game() _start_game()
elif should_join: elif should_join:
print("Auto-joining to ", join_address, " due to --join argument") # Check network mode after it's been set
var current_mode = network_manager.network_mode
if join_address.is_empty() and (current_mode == 1 or current_mode == 2):
# No address provided, and using WebRTC or WebSocket - fetch and auto-join first available room
LogManager.log("Auto-joining: No address provided, fetching available rooms (mode: " + str(current_mode) + ")...", LogManager.CATEGORY_UI)
network_manager.set_local_player_count(local_count)
is_auto_joining = true
# Create timer for retrying room fetches
room_fetch_timer = Timer.new()
room_fetch_timer.name = "RoomFetchTimer"
room_fetch_timer.wait_time = 2.0 # Retry every 2 seconds
room_fetch_timer.timeout.connect(_retry_room_fetch)
room_fetch_timer.autostart = false
add_child(room_fetch_timer)
# Connect to rooms_fetched signal (not one-shot, so we can keep retrying)
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
network_manager.rooms_fetched.connect(_on_rooms_fetched_auto_join)
# Show room fetch status UI and start fetching
_show_room_fetch_status()
_start_room_fetch()
elif not join_address.is_empty():
LogManager.log("Auto-joining to " + join_address + " due to --join argument", LogManager.CATEGORY_UI)
address_input.text = join_address address_input.text = join_address
network_manager.set_local_player_count(local_count) network_manager.set_local_player_count(local_count)
if network_manager.join_game(join_address): if network_manager.join_game(join_address):
# Connection callback will handle starting the game # Connection callback will handle starting the game
pass pass
else:
# ENet mode without address - need a default address
var default_address = "127.0.0.1"
LogManager.log("Auto-joining to " + default_address + " due to --join argument (ENet mode, no address provided)", LogManager.CATEGORY_UI)
address_input.text = default_address
network_manager.set_local_player_count(local_count)
if network_manager.join_game(default_address):
# Connection callback will handle starting the game
pass
func _on_rooms_fetched_display(rooms: Array):
"""Display available rooms when fetched (non-auto-join mode)"""
# Only handle if not in auto-join mode (auto-join has its own handler)
if is_auto_joining:
return # Let auto-join handler take care of it
# Hide loading indicator - request completed
_hide_loading_indicator()
# Update last fetch time
_update_last_fetch_time()
# Clear existing room list
_clear_room_list()
# Display rooms if any are found
if rooms.is_empty():
LogManager.log("GameUI: No available rooms found", LogManager.CATEGORY_UI)
# Keep room fetch status visible so user can see "Last fetched: ..." even with no rooms
# Add a "No rooms available" message
_add_room_list_header("No available rooms")
else:
LogManager.log("GameUI: Found " + str(rooms.size()) + " available room(s)", LogManager.CATEGORY_UI)
# Add header
_add_room_list_header("Available Rooms:")
# Add each room to the list
for room in rooms:
var room_code = room.get("room", "")
var players = room.get("players", 0)
var level = room.get("level", 1)
_add_room_item(room_code, players, level)
func _on_rooms_fetched_auto_join(rooms: Array):
"""Auto-join the first available room when --join --webrtc is used without address"""
if not is_auto_joining:
return # Not in auto-join mode, ignore
# Hide loading indicator - request completed
_hide_loading_indicator()
# Update last fetch time
_update_last_fetch_time()
if rooms.is_empty():
LogManager.log("No available rooms found, will retry in 2 seconds...", LogManager.CATEGORY_UI)
# Start timer to retry fetching
if room_fetch_timer:
room_fetch_timer.start()
return
# Stop retrying - we found rooms!
if room_fetch_timer:
room_fetch_timer.stop()
is_auto_joining = false
# Hide room fetch status UI
_hide_room_fetch_status()
# Disconnect from signal since we're done
if network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
network_manager.rooms_fetched.disconnect(_on_rooms_fetched_auto_join)
# Sort rooms by player count (prefer rooms with more players, but not full)
rooms.sort_custom(func(a, b): return a.get("players", 0) < b.get("players", 0))
# Try to join the first room
var room = rooms[0]
var room_code = room.get("room", "")
if room_code.is_empty():
LogManager.log("Room code is empty, cannot join - will retry", LogManager.CATEGORY_UI)
# Restart timer to retry
if room_fetch_timer:
room_fetch_timer.start()
is_auto_joining = true
# Keep showing status UI
return
# Get local player count from spinbox (same as _on_join_pressed does)
var local_count = int(local_players_spinbox.value)
LogManager.log("Auto-joining room: " + room_code + " (players: " + str(room.get("players", 0)) + ", level: " + str(room.get("level", 1)) + ")", LogManager.CATEGORY_UI)
address_input.text = room_code
network_manager.set_local_player_count(local_count)
if network_manager.join_game(room_code):
# Connection callback will handle starting the game
pass
func _retry_room_fetch():
"""Retry fetching available rooms"""
if not is_auto_joining:
if room_fetch_timer:
room_fetch_timer.stop()
return
LogManager.log("Retrying room fetch...", LogManager.CATEGORY_UI)
_start_room_fetch()
func _start_room_fetch():
"""Start fetching rooms and show loading indicator"""
# Only fetch if WebRTC mode and not hosting
if network_manager.network_mode != 1: # Not WebRTC
return
if is_hosting or network_manager.is_hosting:
LogManager.log("GameUI: Skipping room fetch - we are hosting", LogManager.CATEGORY_UI)
return
# Show the status container and loading indicator
_show_room_fetch_status()
_show_loading_indicator()
# Check if a request is already in progress before starting a new one
var fetch_result = network_manager.fetch_available_rooms()
if not fetch_result:
# Request failed or is already in progress - hide loading indicator
_hide_loading_indicator()
# Start timer to retry (only in auto-join mode)
if is_auto_joining and room_fetch_timer:
room_fetch_timer.start()
func _show_room_fetch_status():
"""Show the room fetch status UI"""
if room_fetch_status_container:
room_fetch_status_container.visible = true
if last_fetch_label:
last_fetch_label.text = "Last fetched: Never"
# Create room list container if it doesn't exist
_create_room_list_container()
# Create refresh button if it doesn't exist
_create_refresh_button()
func _show_loading_indicator():
"""Show the loading indicator"""
if loading_label:
loading_label.text = "Loading rooms..."
if loading_spinner:
loading_spinner.text = ""
func _hide_loading_indicator():
"""Hide the loading indicator"""
if loading_label:
loading_label.text = ""
if loading_spinner:
loading_spinner.text = ""
func _hide_room_fetch_status():
"""Hide the room fetch status UI"""
if room_fetch_status_container:
room_fetch_status_container.visible = false
func _create_refresh_button():
"""Create a refresh button for manually reloading the room list"""
if refresh_button:
return # Already exists
if not room_fetch_status_container:
return
# Create HBoxContainer for refresh button (below last fetch label)
var refresh_container = HBoxContainer.new()
refresh_container.name = "RefreshButtonContainer"
refresh_container.alignment = BoxContainer.ALIGNMENT_END
# Create refresh button
refresh_button = Button.new()
refresh_button.name = "RefreshButton"
refresh_button.text = "Refresh Rooms"
refresh_button.pressed.connect(_on_refresh_button_pressed)
refresh_container.add_child(refresh_button)
# Add refresh container after LastFetchLabel (or at the end if LastFetchLabel doesn't exist)
var last_fetch_node = room_fetch_status_container.get_node_or_null("LastFetchLabel")
if last_fetch_node:
var index = last_fetch_node.get_index() + 1
room_fetch_status_container.add_child(refresh_container)
room_fetch_status_container.move_child(refresh_container, index)
else:
room_fetch_status_container.add_child(refresh_container)
# Create cooldown timer
refresh_cooldown_timer = Timer.new()
refresh_cooldown_timer.name = "RefreshCooldownTimer"
refresh_cooldown_timer.wait_time = 5.0
refresh_cooldown_timer.one_shot = true
refresh_cooldown_timer.timeout.connect(_on_refresh_cooldown_finished)
refresh_cooldown_timer.autostart = false
add_child(refresh_cooldown_timer)
func _on_refresh_button_pressed():
"""Handle refresh button click"""
# Disable button and start cooldown
if refresh_button:
refresh_button.disabled = true
refresh_button.text = "Refreshing..."
if refresh_cooldown_timer:
refresh_cooldown_timer.start()
# Fetch rooms
_start_room_fetch()
func _on_refresh_cooldown_finished():
"""Re-enable refresh button after cooldown"""
if refresh_button:
refresh_button.disabled = false
refresh_button.text = "Refresh Rooms"
func _update_last_fetch_time():
"""Update the last fetch time label with current datetime"""
if last_fetch_label:
var now = Time.get_datetime_dict_from_system()
var time_str = "%02d:%02d:%02d" % [now.hour, now.minute, now.second]
last_fetch_label.text = "Last fetched: " + time_str
func _create_room_list_container():
"""Create the container for displaying available rooms"""
if room_list_container:
return # Already exists
if not room_fetch_status_container:
return
# Create a ScrollContainer for the room list
var scroll_container = ScrollContainer.new()
scroll_container.name = "RoomListScrollContainer"
scroll_container.custom_minimum_size = Vector2(0, 150) # Set a reasonable height
scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
# Create VBoxContainer inside scroll container
room_list_container = VBoxContainer.new()
room_list_container.name = "RoomListContainer"
room_list_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll_container.add_child(room_list_container)
# Add scroll container to room_fetch_status_container (after LastFetchLabel)
room_fetch_status_container.add_child(scroll_container)
func _clear_room_list():
"""Clear all room items from the list"""
if room_list_container:
for child in room_list_container.get_children():
child.queue_free()
func _add_room_list_header(text: String):
"""Add a header label to the room list"""
if not room_list_container:
_create_room_list_container()
if not room_list_container:
return
var label = Label.new()
label.text = text
label.add_theme_font_size_override("font_size", 14)
room_list_container.add_child(label)
func _add_room_item(room_code: String, players: int, level: int):
"""Add a room item with join button to the list"""
if not room_list_container:
_create_room_list_container()
if not room_list_container:
return
# Create HBoxContainer for this room
var room_row = HBoxContainer.new()
room_row.name = "RoomRow_" + room_code
# Room code label
var code_label = Label.new()
code_label.text = room_code
code_label.custom_minimum_size = Vector2(100, 0)
code_label.add_theme_font_size_override("font_size", 12)
room_row.add_child(code_label)
# Info label (players, level)
var info_label = Label.new()
info_label.text = "Players: %d | Level: %d" % [players, level]
info_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
info_label.add_theme_font_size_override("font_size", 12)
room_row.add_child(info_label)
# Join button
var room_join_button = Button.new()
room_join_button.text = "Join"
room_join_button.custom_minimum_size = Vector2(80, 0)
# Connect button to join function
room_join_button.pressed.connect(func(): _join_room(room_code))
room_row.add_child(room_join_button)
room_list_container.add_child(room_row)
func _join_room(room_code: String):
"""Join a room by setting the address and clicking join"""
if room_code.is_empty():
return
# Set the address input
if address_input:
address_input.text = room_code
# Get local player count
var local_count = int(local_players_spinbox.value)
network_manager.set_local_player_count(local_count)
# Join the game
if network_manager.join_game(room_code):
LogManager.log("Joining room: " + room_code, LogManager.CATEGORY_UI)
func _on_network_mode_changed(index: int): func _on_network_mode_changed(index: int):
# On web builds, index 0 = WebRTC, index 1 = WebSocket # On web builds, index 0 = WebRTC, index 1 = WebSocket
@@ -153,9 +517,17 @@ func _on_network_mode_changed(index: int):
address_input.placeholder_text = "Enter Room Code (e.g., ABC123)" address_input.placeholder_text = "Enter Room Code (e.g., ABC123)"
var mode_names = ["ENet", "WebRTC", "WebSocket"] var mode_names = ["ENet", "WebRTC", "WebSocket"]
print("GameUI: Network mode changed to: ", mode_names[actual_mode]) LogManager.log("GameUI: Network mode changed to: " + mode_names[actual_mode], LogManager.CATEGORY_UI)
# If WebRTC is selected, fetch available rooms (unless we're auto-joining or hosting)
if actual_mode == 1 and not is_auto_joining and not is_hosting: # WebRTC mode
_start_room_fetch()
elif actual_mode != 1: # Not WebRTC mode
# Hide room fetch status if switching away from WebRTC
_hide_room_fetch_status()
func _on_host_pressed(): func _on_host_pressed():
is_hosting = true # Set flag so we don't fetch rooms
var local_count = int(local_players_spinbox.value) var local_count = int(local_players_spinbox.value)
network_manager.set_local_player_count(local_count) network_manager.set_local_player_count(local_count)
@@ -171,42 +543,115 @@ func _on_host_pressed():
_start_game() _start_game()
func _on_join_pressed(): func _on_join_pressed():
# Reset error state when attempting new connection
connection_error_shown = false
_hide_connection_error()
is_joining_attempt = true
var address = address_input.text var address = address_input.text
if address.is_empty(): if address.is_empty():
var mode = network_manager.network_mode var mode = network_manager.network_mode
if mode == 1 or mode == 2: # WebRTC or WebSocket if mode == 1 or mode == 2: # WebRTC or WebSocket
print("Error: Please enter a room code") LogManager.log("Error: Please enter a room code", LogManager.CATEGORY_UI)
return return
else: # ENet else: # ENet mode without address - use default
address = "127.0.0.1" address = "127.0.0.1"
var local_count = int(local_players_spinbox.value) var local_count = int(local_players_spinbox.value)
network_manager.set_local_player_count(local_count) network_manager.set_local_player_count(local_count)
if network_manager.join_game(address): if network_manager.join_game(address):
last_join_address = address
var mode = network_manager.network_mode var mode = network_manager.network_mode
if mode == 1: # WebRTC if mode == 1: # WebRTC
print("Joining WebRTC game with room code: ", address) LogManager.log("Joining WebRTC game with room code: " + address, LogManager.CATEGORY_UI)
elif mode == 2: # WebSocket elif mode == 2: # WebSocket
print("Joining WebSocket game with room code: ", address) LogManager.log("Joining WebSocket game with room code: " + address, LogManager.CATEGORY_UI)
else: # ENet else: # ENet
print("Joining ENet game at ", address, " with ", local_count, " local players") LogManager.log("Joining ENet game at " + address + " with " + str(local_count) + " local players", LogManager.CATEGORY_UI)
func _on_connection_succeeded(): func _on_connection_succeeded():
print("Connection succeeded, starting game") LogManager.log("GameUI: Connection succeeded signal received, starting game", LogManager.CATEGORY_UI)
_start_game() is_joining_attempt = false
# Check if node is still valid before starting game
if not is_inside_tree():
LogManager.log_error("GameUI: Cannot start game - node not in tree", LogManager.CATEGORY_UI)
return
# Use call_deferred to ensure we're in a safe state to change scenes
call_deferred("_start_game")
func _on_connection_failed(): func _on_connection_failed():
print("Connection failed") LogManager.log("Connection failed", LogManager.CATEGORY_UI)
# Show error message if connection_error_shown:
var error_label = Label.new() # Already shown, don't spam
error_label.text = "Failed to connect to server" return
error_label.modulate = Color.RED
main_menu.add_child(error_label) connection_error_shown = true
var mode = network_manager.network_mode
var mode_name = "ENet"
if mode == 1:
mode_name = "WebRTC"
elif mode == 2:
mode_name = "WebSocket"
if is_joining_attempt:
var code_hint = (" (" + last_join_address + ")") if not last_join_address.is_empty() else ""
_show_connection_error("Failed to join room" + code_hint + ". Did you enter the correct code?")
# For WebRTC, refresh signaling connection after a failed join
if mode == 1 and network_manager and network_manager.has_method("refresh_webrtc_servers"):
network_manager.refresh_webrtc_servers(last_join_address)
# Show room list UI and refresh rooms after a failed WebRTC join
_show_room_fetch_status()
_show_loading_indicator()
_start_room_fetch()
else:
_show_connection_error("Connection failed (" + mode_name + "). Please try again.")
func _show_connection_error(message: String):
"""Show a temporary error message in the UI"""
if not main_menu:
return
# Remove existing error label if any
_hide_connection_error()
# Create error label
connection_error_label = Label.new()
connection_error_label.name = "ConnectionErrorLabel"
connection_error_label.text = message
connection_error_label.modulate = Color.RED
connection_error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
connection_error_label.add_theme_font_size_override("font_size", 14)
# Add to main menu VBoxContainer (after the title)
var vbox = main_menu.get_node_or_null("VBoxContainer")
if vbox:
# Insert after title (index 0) or at the beginning
vbox.add_child(connection_error_label)
vbox.move_child(connection_error_label, 1) # Move to position 1 (after title)
# Auto-hide after 5 seconds
await get_tree().create_timer(5.0).timeout
_hide_connection_error()
func _hide_connection_error():
"""Hide and remove the connection error label"""
if connection_error_label:
connection_error_label.queue_free()
connection_error_label = null
func _start_game(): func _start_game():
# Check if node is still in the tree before trying to access get_tree()
if not is_inside_tree():
LogManager.log_error("GameUI: Cannot change scene - node is not in tree", LogManager.CATEGORY_UI)
return
# Hide menu # Hide menu
if main_menu:
main_menu.visible = false main_menu.visible = false
# Load the game scene # Load the game scene
get_tree().change_scene_to_file("res://scenes/game_world.tscn") var tree = get_tree()
if tree:
tree.change_scene_to_file("res://scenes/game_world.tscn")
else:
LogManager.log_error("GameUI: Cannot change scene - get_tree() is null", LogManager.CATEGORY_UI)

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@ var texture_progress_bar_boss_hp: TextureProgressBar = null
var label_host: Label = null var label_host: Label = null
var label_player_count: Label = null var label_player_count: Label = null
var label_room_code: Label = null var label_room_code: Label = null
var label_disconnected: Label = null
var game_world: Node = null var game_world: Node = null
var network_manager: Node = null var network_manager: Node = null
@@ -41,6 +42,7 @@ func _ready():
label_host = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelHost") label_host = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelHost")
label_player_count = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelPlayerCount") label_player_count = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelPlayerCount")
label_room_code = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelRoomCode") label_room_code = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelRoomCode")
label_disconnected = get_node_or_null("CenterTop/LabelDisconnected")
# Find network manager # Find network manager
network_manager = get_node_or_null("/root/NetworkManager") network_manager = get_node_or_null("/root/NetworkManager")
@@ -48,6 +50,8 @@ func _ready():
# Connect to player connection signals to update player count # Connect to player connection signals to update player count
network_manager.player_connected.connect(_on_player_connected) network_manager.player_connected.connect(_on_player_connected)
network_manager.player_disconnected.connect(_on_player_disconnected) network_manager.player_disconnected.connect(_on_player_disconnected)
network_manager.connection_failed.connect(_on_connection_failed)
network_manager.connection_succeeded.connect(_on_connection_succeeded)
# Debug: Check if nodes were found # Debug: Check if nodes were found
if not label_time_value: if not label_time_value:
@@ -86,6 +90,21 @@ func _on_player_connected(_peer_id: int, _player_info: Dictionary):
func _on_player_disconnected(_peer_id: int, _player_info: Dictionary): func _on_player_disconnected(_peer_id: int, _player_info: Dictionary):
_update_host_info() _update_host_info()
func _on_connection_failed():
# Show disconnection message
if label_disconnected:
label_disconnected.visible = true
# Show different message for host vs joiner
if network_manager and network_manager.is_hosting:
label_disconnected.text = "Lost connection to Matchbox server - Retrying..."
else:
label_disconnected.text = "Disconnected - Reconnecting..."
func _on_connection_succeeded():
# Hide disconnection message
if label_disconnected:
label_disconnected.visible = false
func _update_host_info(): func _update_host_info():
if not network_manager: if not network_manager:
return return

View File

@@ -17,6 +17,7 @@ var is_being_held: bool = false
var held_by_player = null var held_by_player = null
var is_frozen: bool = false var is_frozen: bool = false
var thrown_by_player = null # Track who threw this box var thrown_by_player = null # Track who threw this box
var is_broken: bool = false
# Physics for thrown objects # Physics for thrown objects
var throw_velocity: Vector2 = Vector2.ZERO var throw_velocity: Vector2 = Vector2.ZERO
@@ -47,6 +48,17 @@ func _ready():
collision_layer = 2 # Layer 2 for objects collision_layer = 2 # Layer 2 for objects
collision_mask = 1 | 2 | 4 # Collide with players, other objects, and walls collision_mask = 1 | 2 | 4 # Collide with players, other objects, and walls
# Ensure deterministic name for network sync
if has_meta("object_index") and not name.begins_with("InteractableObject_"):
name = "InteractableObject_%d" % get_meta("object_index")
elif name.begins_with("InteractableObject_"):
# Ensure meta matches name if it already has a consistent name
var index_str = name.substr(20)
if index_str.is_valid_int():
var name_index = index_str.to_int()
if not has_meta("object_index") or get_meta("object_index") != name_index:
set_meta("object_index", name_index)
# No gravity in top-down # No gravity in top-down
motion_mode = MOTION_MODE_FLOATING motion_mode = MOTION_MODE_FLOATING
@@ -164,9 +176,14 @@ func _handle_air_collision():
# Box breaks (only if destroyable) # Box breaks (only if destroyable)
if is_destroyable: if is_destroyable:
# Sync break to OTHER clients via RPC BEFORE breaking locally
# Use game_world to route the RPC to avoid node path resolution issues
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_rpc_to_ready_peers"):
game_world._rpc_to_ready_peers("_sync_object_break", [name])
_break_into_pieces() _break_into_pieces()
if multiplayer.has_multiplayer_peer():
_sync_break.rpc()
return return
@@ -181,11 +198,14 @@ func _handle_air_collision():
# Hit a player! Break locally and sync to others (only if destroyable) # Hit a player! Break locally and sync to others (only if destroyable)
if is_destroyable: if is_destroyable:
_break_into_pieces() # Sync break to OTHER clients via RPC BEFORE breaking locally
# Use game_world to route the RPC to avoid node path resolution issues
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_rpc_to_ready_peers"):
game_world._rpc_to_ready_peers("_sync_object_break", [name])
# Sync break to OTHER clients _break_into_pieces()
if multiplayer.has_multiplayer_peer():
_sync_break.rpc()
# Damage and knockback player using RPC # Damage and knockback player using RPC
# Pass the thrower's position for accurate direction # Pass the thrower's position for accurate direction
@@ -214,24 +234,28 @@ func _handle_air_collision():
# Hit another box! Break both locally (only if destroyable) # Hit another box! Break both locally (only if destroyable)
if is_destroyable: if is_destroyable:
# Sync break to OTHER clients via RPC BEFORE breaking locally
# Use game_world to route the RPC to avoid node path resolution issues
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_rpc_to_ready_peers"):
game_world._rpc_to_ready_peers("_sync_object_break", [name])
# Tell the other box to break too
if collider.has_method("can_be_destroyed") and collider.can_be_destroyed():
game_world._rpc_to_ready_peers("_sync_object_break", [collider.name])
_break_into_pieces() _break_into_pieces()
if collider.has_method("_break_into_pieces") and collider.has_method("can_be_destroyed") and collider.can_be_destroyed(): if collider.has_method("_break_into_pieces") and collider.has_method("can_be_destroyed") and collider.can_be_destroyed():
collider._break_into_pieces() collider._break_into_pieces()
# Sync break to OTHER clients
if multiplayer.has_multiplayer_peer():
_sync_break.rpc()
# Tell the other box to break too
if collider.has_method("_sync_break") and collider.has_method("can_be_destroyed") and collider.can_be_destroyed():
collider._sync_break.rpc()
print(name, " hit another box!") print(name, " hit another box!")
return return
func _break_into_pieces(): func _break_into_pieces(silent: bool = false):
# Only break if destroyable # Only break if destroyable
if not is_destroyable: if not is_destroyable or is_broken:
return return
is_broken = true
var sprite_texture = $Sprite2D.texture var sprite_texture = $Sprite2D.texture
var frame_width = sprite_texture.get_width() / $Sprite2D.hframes var frame_width = sprite_texture.get_width() / $Sprite2D.hframes
@@ -254,6 +278,7 @@ func _break_into_pieces():
Rect2(frame_x + frame_width / 2, frame_y + frame_height / 2, frame_width / 2, frame_height / 2) # Bottom-right Rect2(frame_x + frame_width / 2, frame_y + frame_height / 2, frame_width / 2, frame_height / 2) # Bottom-right
] ]
if not silent:
for i in range(4): for i in range(4):
var tp = tileParticleScene.instantiate() as CharacterBody2D var tp = tileParticleScene.instantiate() as CharacterBody2D
var spr2D = tp.get_node("Sprite2D") as Sprite2D var spr2D = tp.get_node("Sprite2D") as Sprite2D
@@ -292,6 +317,7 @@ func _break_into_pieces():
ItemLootHelper.spawn_item_loot(item, global_position, entities_node, game_world) ItemLootHelper.spawn_item_loot(item, global_position, entities_node, game_world)
print(name, " dropped item: ", item.item_name, " when broken") print(name, " dropped item: ", item.item_name, " when broken")
if not silent:
if ($SfxShatter.playing): if ($SfxShatter.playing):
await $SfxShatter.finished await $SfxShatter.finished
if ($SfxBreakCrate.playing): if ($SfxBreakCrate.playing):
@@ -302,8 +328,36 @@ func _break_into_pieces():
func can_be_grabbed() -> bool: func can_be_grabbed() -> bool:
return is_grabbable and not is_being_held return is_grabbable and not is_being_held
func _get_configured_object_type() -> String:
# Prefer the configured type from dungeon data if available
var idx = -1
if name.begins_with("InteractableObject_"):
var index_str = name.substr(20)
if index_str.is_valid_int():
idx = index_str.to_int()
elif has_meta("object_index"):
idx = get_meta("object_index")
if idx >= 0:
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and "dungeon_data" in game_world and game_world.dungeon_data.has("interactable_objects"):
var objects = game_world.dungeon_data.interactable_objects
if idx < objects.size():
var obj_data = objects[idx]
if obj_data is Dictionary and obj_data.has("type"):
return obj_data.type
return object_type
func can_be_lifted() -> bool: func can_be_lifted() -> bool:
# Can be lifted if it's liftable (being held is OK - we're checking if it CAN be lifted) # Can be lifted if it's liftable (being held is OK - we're checking if it CAN be lifted)
var resolved_type = object_type
if resolved_type == "":
resolved_type = _get_configured_object_type()
if resolved_type in ["Box", "Pot", "LiftableBarrel"]:
return true
if resolved_type in ["Chest", "Pillar", "PushableBarrel", "PushableHighBox"]:
return false
return is_liftable return is_liftable
func can_be_thrown() -> bool: func can_be_thrown() -> bool:
@@ -321,8 +375,16 @@ func on_grabbed(by_player):
# Client - send request to server # Client - send request to server
if by_player: if by_player:
var player_peer_id = by_player.get_multiplayer_authority() var player_peer_id = by_player.get_multiplayer_authority()
print("Chest: Client sending RPC to open chest, player_peer_id: ", player_peer_id) # Use consistent object name based on object_index to avoid NodePath issues
_request_chest_open.rpc_id(1, player_peer_id) var chest_name = name
if has_meta("object_index"):
chest_name = "InteractableObject_%d" % get_meta("object_index")
print("Chest: Client sending RPC to open chest, player_peer_id: ", player_peer_id, " chest_name: ", chest_name)
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_request_chest_open_by_name"):
game_world._request_chest_open_by_name.rpc_id(1, chest_name, player_peer_id)
else:
push_warning("Chest: GameWorld not ready, cannot send chest open request for " + chest_name)
else: else:
# Server or single player - open directly # Server or single player - open directly
_open_chest(by_player) _open_chest(by_player)
@@ -397,10 +459,10 @@ func _sync_box_state(pos: Vector2, vel: Vector2, z_pos: float, z_vel: float, air
is_airborne = airborne is_airborne = airborne
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_break(): func _sync_break(silent: bool = false):
# Sync break to all clients including server (called by whoever breaks the box) # Sync break to all clients including server (called by whoever breaks the box)
if not is_queued_for_deletion(): if not is_queued_for_deletion() and not is_broken:
_break_into_pieces() _break_into_pieces(silent)
# Object type setup functions # Object type setup functions
func setup_pot(): func setup_pot():
@@ -448,7 +510,21 @@ func setup_box():
var box_frames = [7, 26] var box_frames = [7, 26]
if sprite: if sprite:
sprite.frame = box_frames[randi() % box_frames.size()] # Use deterministic randomness based on dungeon seed and position
# This ensures host and clients get the same box variant
var box_seed = 0
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and "dungeon_seed" in game_world:
box_seed = game_world.dungeon_seed
# Add position and object_index to seed to make each box unique but deterministic
box_seed += int(global_position.x) * 1000 + int(global_position.y)
if has_meta("object_index"):
box_seed += get_meta("object_index") * 10000
var rng = RandomNumberGenerator.new()
rng.seed = box_seed
var index = rng.randi() % box_frames.size()
sprite.frame = box_frames[index]
func setup_chest(): func setup_chest():
object_type = "Chest" object_type = "Chest"
@@ -503,7 +579,19 @@ func setup_pushable_high_box():
var bottom_frames = [24, 25] var bottom_frames = [24, 25]
var top_frames = [5, 6] var top_frames = [5, 6]
var index = randi() % bottom_frames.size()
# Use deterministic randomness based on dungeon seed and position
# This ensures host and clients get the same chest variant
var highbox_seed = 0
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and "dungeon_seed" in game_world:
highbox_seed = game_world.dungeon_seed
# Add position to seed to make each chest unique but deterministic
highbox_seed += int(global_position.x) * 1000 + int(global_position.y)
var rng = RandomNumberGenerator.new()
rng.seed = highbox_seed
var index = rng.randi() % bottom_frames.size()
if sprite: if sprite:
sprite.frame = bottom_frames[index] sprite.frame = bottom_frames[index]
@@ -519,6 +607,14 @@ func _open_chest(by_player: Node = null):
return return
$SfxOpenChest.play() $SfxOpenChest.play()
is_chest_opened = true is_chest_opened = true
# Track opened chest for syncing to new clients
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and has_meta("object_index"):
var obj_index = get_meta("object_index")
game_world.opened_chests[obj_index] = true
LogManager.log("Chest: Tracked opened chest with index " + str(obj_index), LogManager.CATEGORY_NETWORK)
if sprite and chest_opened_frame >= 0: if sprite and chest_opened_frame >= 0:
sprite.frame = chest_opened_frame sprite.frame = chest_opened_frame
@@ -579,7 +675,12 @@ func _open_chest(by_player: Node = null):
# Sync chest opening visual to all clients (item already given on server) # Sync chest opening visual to all clients (item already given on server)
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
var player_peer_id = by_player.get_multiplayer_authority() if by_player else 0 var player_peer_id = by_player.get_multiplayer_authority() if by_player else 0
_sync_chest_open.rpc(selected_loot.type if by_player else "coin", player_peer_id) var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_rpc_to_ready_peers"):
var chest_name = name
if has_meta("object_index"):
chest_name = "InteractableObject_%d" % get_meta("object_index")
game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, selected_loot.type if by_player else "coin", player_peer_id])
else: else:
push_error("Chest: ERROR - No valid player to give item to!") push_error("Chest: ERROR - No valid player to give item to!")

View File

@@ -67,7 +67,7 @@ static func spawn_item_loot(item: Item, position: Vector2, entities_node: Node,
loot.set_meta("loot_id", loot_id) loot.set_meta("loot_id", loot_id)
# Sync item data to clients # Sync item data to clients
game_world._sync_item_loot_spawn.rpc(safe_spawn_pos, item.save(), initial_velocity, random_velocity_z, loot_id) game_world._rpc_to_ready_peers("_sync_item_loot_spawn", [safe_spawn_pos, item.save(), initial_velocity, random_velocity_z, loot_id])
print("ItemLootHelper: Spawned item loot: ", item.item_name, " at ", safe_spawn_pos) print("ItemLootHelper: Spawned item loot: ", item.item_name, " at ", safe_spawn_pos)
return loot return loot

View File

@@ -0,0 +1,80 @@
extends Node
# Log Manager - Centralized logging system with category-based filtering
# Categories can be enabled/disabled independently for debugging
# Log categories
const CATEGORY_NETWORK = "NETWORK"
const CATEGORY_GAMEPLAY = "GAMEPLAY"
const CATEGORY_UI = "UI"
const CATEGORY_DUNGEON = "DUNGEON"
const CATEGORY_ENEMY = "ENEMY"
const CATEGORY_PLAYER = "PLAYER"
const CATEGORY_DOOR = "DOOR"
const CATEGORY_INVENTORY = "INVENTORY"
const CATEGORY_DEFAULT = "DEFAULT"
# Enable/disable flags for each category
var enabled_categories: Dictionary = {
CATEGORY_NETWORK: true, # Enable network logs for debugging
CATEGORY_GAMEPLAY: false,
CATEGORY_UI: false,
CATEGORY_DUNGEON: false,
CATEGORY_ENEMY: false,
CATEGORY_PLAYER: false,
CATEGORY_DOOR: false,
CATEGORY_INVENTORY: false,
CATEGORY_DEFAULT: false, # Disable default/unclassified logs
}
# Logging helper to determine host/joiner prefix
func _log_prefix() -> String:
var network_manager = get_node_or_null("/root/NetworkManager")
if network_manager and network_manager.is_hosting:
return "[H] "
else:
return "[J] "
func log(message: String, category: String = CATEGORY_DEFAULT):
"""Log a message if the category is enabled"""
if enabled_categories.get(category, false):
print(_log_prefix() + "[" + category + "] " + message)
func log_error(message: String, category: String = CATEGORY_DEFAULT):
"""Log an error message if the category is enabled"""
if enabled_categories.get(category, false):
push_error(_log_prefix() + "[" + category + "] " + message)
func set_category_enabled(category: String, enabled: bool):
"""Enable or disable a log category"""
enabled_categories[category] = enabled
func is_category_enabled(category: String) -> bool:
"""Check if a category is enabled"""
return enabled_categories.get(category, false)
func enable_all_categories():
"""Enable all log categories"""
for category in enabled_categories:
enabled_categories[category] = true
func disable_all_categories():
"""Disable all log categories"""
for category in enabled_categories:
enabled_categories[category] = false
func enable_network_only():
"""Enable only network-related logs (useful for debugging multiplayer)"""
disable_all_categories()
enabled_categories[CATEGORY_NETWORK] = true
# Example usage:
# LogManager.log("This is a network message", LogManager.CATEGORY_NETWORK)
# LogManager.log("This is a gameplay message", LogManager.CATEGORY_GAMEPLAY)
# LogManager.set_category_enabled(LogManager.CATEGORY_GAMEPLAY, true) # Enable gameplay logs
# LogManager.enable_network_only() # Disable all except network
#
# NOTE: Many scripts still use print() directly. To suppress those logs,
# you can temporarily redirect print() output or update scripts to use LogManager.
# For now, network-related scripts (NetworkManager, MatchboxClient, RoomRegistry)
# use LogManager, while gameplay scripts still use print() directly.

View File

@@ -0,0 +1 @@
uid://db2vxtkt6gnvy

View File

@@ -379,11 +379,10 @@ func _process_pickup_on_server(player: Node):
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_loot_remove"): if game_world and game_world.has_method("_sync_loot_remove"):
print("Loot: Server syncing removal of loot id=", loot_id, " at ", global_position) print("Loot: Server syncing removal of loot id=", loot_id, " at ", global_position)
game_world._sync_loot_remove.rpc(loot_id, global_position) game_world._rpc_to_ready_peers("_sync_loot_remove", [loot_id, global_position])
else: else:
# Fallback: try direct RPC (may fail if node path doesn't match) # If GameWorld isn't ready, skip removal sync to avoid node path RPC errors
print("Loot: Server syncing removal via direct RPC (fallback)") print("Loot: GameWorld not ready, skipping removal sync for loot id=", loot_id)
rpc("_sync_remove")
match loot_type: match loot_type:
LootType.COIN: LootType.COIN:
@@ -395,9 +394,11 @@ func _process_pickup_on_server(player: Node):
# Show floating text with item graphic and text # Show floating text with item graphic and text
var coin_texture = load("res://assets/gfx/pickups/gold_coin.png") var coin_texture = load("res://assets/gfx/pickups/gold_coin.png")
_show_floating_text(player, "+" + str(coin_value) + " COIN", Color(1.0, 0.84, 0.0), 0.5, 0.5, coin_texture, 6, 1, 0) _show_floating_text(player, "+" + str(coin_value) + " COIN", Color(1.0, 0.84, 0.0), 0.5, 0.5, coin_texture, 6, 1, 0)
# Sync floating text to client # Sync floating text to client via GameWorld to avoid loot node path RPCs
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1: if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(coin_value) + " COIN", Color(1.0, 0.84, 0.0), coin_value, 0, player.get_multiplayer_authority()) var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_loot_floating_text"):
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(coin_value) + " COIN", Color(1.0, 0.84, 0.0), 0, player.get_multiplayer_authority())
self.visible = false self.visible = false
@@ -416,9 +417,11 @@ func _process_pickup_on_server(player: Node):
# Show floating text with item graphic and heal amount # Show floating text with item graphic and heal amount
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 10) _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 10)
# Sync floating text to client # Sync floating text to client via GameWorld to avoid loot node path RPCs
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1: if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, int(actual_heal), (8 * 20) + 10, player.get_multiplayer_authority()) var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_loot_floating_text"):
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, (8 * 20) + 10, player.get_multiplayer_authority())
self.visible = false self.visible = false
@@ -437,9 +440,11 @@ func _process_pickup_on_server(player: Node):
# Show floating text with item graphic and heal amount # Show floating text with item graphic and heal amount
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 11) _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 11)
# Sync floating text to client # Sync floating text to client via GameWorld to avoid loot node path RPCs
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1: if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, int(actual_heal), (8 * 20) + 11, player.get_multiplayer_authority()) var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_loot_floating_text"):
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, (8 * 20) + 11, player.get_multiplayer_authority())
self.visible = false self.visible = false
@@ -458,9 +463,11 @@ func _process_pickup_on_server(player: Node):
# Show floating text with item graphic and heal amount # Show floating text with item graphic and heal amount
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 12) _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 12)
# Sync floating text to client # Sync floating text to client via GameWorld to avoid loot node path RPCs
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1: if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, int(actual_heal), (8 * 20) + 12, player.get_multiplayer_authority()) var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_loot_floating_text"):
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, (8 * 20) + 12, player.get_multiplayer_authority())
self.visible = false self.visible = false
@@ -477,9 +484,11 @@ func _process_pickup_on_server(player: Node):
# Show floating text with item graphic and text # Show floating text with item graphic and text
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_floating_text(player, "+1 KEY", Color.YELLOW, 0.5, 0.5, items_texture, 20, 14, (13 * 20) + 10) _show_floating_text(player, "+1 KEY", Color.YELLOW, 0.5, 0.5, items_texture, 20, 14, (13 * 20) + 10)
# Sync floating text to client # Sync floating text to client via GameWorld to avoid loot node path RPCs
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1: if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+1 KEY", Color.YELLOW, 0, (13 * 20) + 10, player.get_multiplayer_authority()) var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_loot_floating_text"):
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+1 KEY", Color.YELLOW, (13 * 20) + 10, player.get_multiplayer_authority())
self.visible = false self.visible = false
@@ -522,9 +531,11 @@ func _process_pickup_on_server(player: Node):
_show_floating_text(player, display_text, text_color, 0.5, 0.5, items_texture, item.spriteFrames.x, item.spriteFrames.y, item.spriteFrame) _show_floating_text(player, display_text, text_color, 0.5, 0.5, items_texture, item.spriteFrames.x, item.spriteFrames.y, item.spriteFrame)
# Sync floating text to client # Sync floating text to client via GameWorld to avoid loot node path RPCs
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1: if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, display_text, text_color, 0, item.spriteFrame, player.get_multiplayer_authority()) var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_loot_floating_text"):
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, display_text, text_color, item.spriteFrame, player.get_multiplayer_authority())
self.visible = false self.visible = false

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ signal player_connected(peer_id, player_info)
signal player_disconnected(peer_id, player_info) signal player_disconnected(peer_id, player_info)
signal connection_failed() signal connection_failed()
signal connection_succeeded() signal connection_succeeded()
signal rooms_fetched(rooms: Array) # Forwarded from room_registry
const DEFAULT_PORT = 21212 const DEFAULT_PORT = 21212
const MAX_PLAYERS = 8 const MAX_PLAYERS = 8
@@ -24,17 +25,32 @@ var network_mode: int = 0 # 0 = ENet, 1 = WebRTC, 2 = WebSocket
var room_id = "" # Room ID for Matchbox (WebRTC) or WebSocket server URL var room_id = "" # Room ID for Matchbox (WebRTC) or WebSocket server URL
var matchbox_client: Node = null # Matchbox client instance var matchbox_client: Node = null # Matchbox client instance
const WEBSOCKET_SERVER_URL = "ws://ruinborn.thefirstboss.com:21212" # WebSocket server URL const WEBSOCKET_SERVER_URL = "ws://ruinborn.thefirstboss.com:21212" # WebSocket server URL
var room_registry: Node = null # Room registry client for room discovery
# Reconnection state (for joiners only)
var reconnection_room_id: String = "" # Store room_id for reconnection
var reconnection_level: int = 0 # Store level for reconnection
var reconnection_attempting: bool = false # Track if we're attempting to reconnect
var reconnection_timer: float = 0.0 # Timer for reconnection delay
const RECONNECTION_DELAY: float = 2.0 # Delay before attempting reconnection
# Logging - use LogManager for categorized logging
func log_print(message: String):
LogManager.log(message, LogManager.CATEGORY_NETWORK)
func log_error(message: String):
LogManager.log_error(message, LogManager.CATEGORY_NETWORK)
func _ready(): func _ready():
# Detect if running in browser - default to WebRTC on web, ENet on native # Detect if running in browser - default to WebRTC on web, ENet on native
if OS.get_name() == "Web": if OS.get_name() == "Web":
network_mode = 1 # WebRTC default for web network_mode = 1 # WebRTC default for web
print("NetworkManager: Detected Web platform, defaulting to WebRTC") log_print("NetworkManager: Detected Web platform, defaulting to WebRTC")
print("NetworkManager: Matchbox server: ", MATCHBOX_SERVER) log_print("NetworkManager: Matchbox server: " + MATCHBOX_SERVER)
else: else:
network_mode = 0 # ENet default for native network_mode = 0 # ENet default for native
print("NetworkManager: Using ENet by default for native platform") log_print("NetworkManager: Using ENet by default for native platform")
print("NetworkManager: You can switch network modes in the menu") log_print("NetworkManager: You can switch network modes in the menu")
# Connect multiplayer signals # Connect multiplayer signals
multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_connected.connect(_on_peer_connected)
@@ -43,25 +59,28 @@ func _ready():
multiplayer.connection_failed.connect(_on_connection_failed) multiplayer.connection_failed.connect(_on_connection_failed)
multiplayer.server_disconnected.connect(_on_server_disconnected) multiplayer.server_disconnected.connect(_on_server_disconnected)
# Create room registry client
var registry_script = load("res://scripts/room_registry_client.gd")
room_registry = Node.new()
room_registry.set_script(registry_script)
add_child(room_registry)
room_registry.rooms_fetched.connect(_on_rooms_fetched)
func set_network_mode(mode: int): func set_network_mode(mode: int):
# 0 = ENet, 1 = WebRTC, 2 = WebSocket # 0 = ENet, 1 = WebRTC, 2 = WebSocket
# WebRTC is only available on web builds # WebRTC is now available on native platforms with webrtc-native extension
if mode == 1 and OS.get_name() != "Web":
push_error("NetworkManager: WebRTC is not available on native platforms. WebRTC only works in web builds. Falling back to ENet.")
network_mode = 0
print("NetworkManager: ENet mode enabled (WebRTC not available on native platforms)")
return
network_mode = mode network_mode = mode
match mode: match mode:
0: 0:
print("NetworkManager: ENet mode enabled") log_print("NetworkManager: ENet mode enabled")
1: 1:
print("NetworkManager: WebRTC mode enabled") log_print("NetworkManager: WebRTC mode enabled")
print("NetworkManager: Matchbox server: ", MATCHBOX_SERVER) log_print("NetworkManager: Matchbox server: " + MATCHBOX_SERVER)
if OS.get_name() != "Web":
log_print("NetworkManager: Using webrtc-native extension for native platform")
2: 2:
print("NetworkManager: WebSocket mode enabled") log_print("NetworkManager: WebSocket mode enabled")
print("NetworkManager: WebSocket server: ", WEBSOCKET_SERVER_URL) log_print("NetworkManager: WebSocket server: " + WEBSOCKET_SERVER_URL)
func force_webrtc_mode(enable: bool): func force_webrtc_mode(enable: bool):
# Legacy function for backwards compatibility # Legacy function for backwards compatibility
@@ -82,9 +101,9 @@ func host_game(port: int = DEFAULT_PORT, matchbox_room: String = "") -> bool:
else: else:
room_id = matchbox_room room_id = matchbox_room
print("NetworkManager: Creating WebRTC host with room ID: ", room_id) log_print("NetworkManager: Creating WebRTC host with room ID: " + room_id)
print("NetworkManager: Share this room code with players!") log_print("NetworkManager: Share this room code with players!")
print("NetworkManager: Matchbox URL: ", MATCHBOX_SERVER, "/", room_id) log_print("NetworkManager: Matchbox URL: " + MATCHBOX_SERVER + "/" + room_id)
# Create Matchbox client # Create Matchbox client
var matchbox_script = load("res://scripts/matchbox_client.gd") var matchbox_script = load("res://scripts/matchbox_client.gd")
@@ -106,7 +125,7 @@ func host_game(port: int = DEFAULT_PORT, matchbox_room: String = "") -> bool:
# Connect to Matchbox room (pass is_hosting = true) # Connect to Matchbox room (pass is_hosting = true)
if not matchbox_client.connect_to_room(room_id, true): if not matchbox_client.connect_to_room(room_id, true):
push_error("Failed to connect to Matchbox room") log_error("Failed to connect to Matchbox room")
return false return false
# Register the host as a player (peer_id 1) # Register the host as a player (peer_id 1)
@@ -124,16 +143,16 @@ func host_game(port: int = DEFAULT_PORT, matchbox_room: String = "") -> bool:
else: else:
room_id = matchbox_room room_id = matchbox_room
print("NetworkManager: Creating WebSocket host with room ID: ", room_id) log_print("NetworkManager: Creating WebSocket host with room ID: " + room_id)
print("NetworkManager: Share this room code with players!") log_print("NetworkManager: Share this room code with players!")
print("NetworkManager: WebSocket URL: ", WEBSOCKET_SERVER_URL, "/", room_id) log_print("NetworkManager: WebSocket URL: " + WEBSOCKET_SERVER_URL + "/" + room_id)
peer = WebSocketMultiplayerPeer.new() peer = WebSocketMultiplayerPeer.new()
var url = WEBSOCKET_SERVER_URL + "/" + room_id var url = WEBSOCKET_SERVER_URL + "/" + room_id
error = peer.create_server(port) error = peer.create_server(port)
if error != OK: if error != OK:
push_error("Failed to create WebSocket server: " + str(error)) log_error("Failed to create WebSocket server: " + str(error))
return false return false
multiplayer.multiplayer_peer = peer multiplayer.multiplayer_peer = peer
@@ -183,8 +202,8 @@ func join_game(address: String, port: int = DEFAULT_PORT) -> bool:
# 'address' is the room ID for WebRTC # 'address' is the room ID for WebRTC
room_id = address room_id = address
print("NetworkManager: Joining WebRTC game with room ID: ", room_id) log_print("NetworkManager: Joining WebRTC game with room ID: " + room_id)
print("NetworkManager: Matchbox URL: ", MATCHBOX_SERVER, "/", room_id) log_print("NetworkManager: Matchbox URL: " + MATCHBOX_SERVER + "/" + room_id)
# Create Matchbox client # Create Matchbox client
var matchbox_script = load("res://scripts/matchbox_client.gd") var matchbox_script = load("res://scripts/matchbox_client.gd")
@@ -204,7 +223,7 @@ func join_game(address: String, port: int = DEFAULT_PORT) -> bool:
# Connect to Matchbox room (pass is_hosting = false) # Connect to Matchbox room (pass is_hosting = false)
if not matchbox_client.connect_to_room(room_id, false): if not matchbox_client.connect_to_room(room_id, false):
push_error("Failed to connect to Matchbox room") log_error("Failed to connect to Matchbox room")
return false return false
return true return true
@@ -213,21 +232,21 @@ func join_game(address: String, port: int = DEFAULT_PORT) -> bool:
# 'address' is the room ID for WebSocket # 'address' is the room ID for WebSocket
room_id = address room_id = address
print("NetworkManager: Joining WebSocket game with room ID: ", room_id) log_print("NetworkManager: Joining WebSocket game with room ID: " + room_id)
print("NetworkManager: WebSocket URL: ", WEBSOCKET_SERVER_URL, "/", room_id) log_print("NetworkManager: WebSocket URL: " + WEBSOCKET_SERVER_URL + "/" + room_id)
peer = WebSocketMultiplayerPeer.new() peer = WebSocketMultiplayerPeer.new()
var url = WEBSOCKET_SERVER_URL + "/" + room_id var url = WEBSOCKET_SERVER_URL + "/" + room_id
error = peer.create_client(url) error = peer.create_client(url)
if error != OK: if error != OK:
push_error("Failed to create WebSocket client: " + str(error)) log_error("Failed to create WebSocket client: " + str(error))
return false return false
multiplayer.multiplayer_peer = peer multiplayer.multiplayer_peer = peer
is_hosting = false is_hosting = false
print("Attempting to connect to WebSocket server: ", url) log_print("Attempting to connect to WebSocket server: " + url)
return true return true
else: # ENet (mode 0) else: # ENet (mode 0)
@@ -236,17 +255,60 @@ func join_game(address: String, port: int = DEFAULT_PORT) -> bool:
error = peer.create_client(address, port) error = peer.create_client(address, port)
if error != OK: if error != OK:
push_error("Failed to create ENet client: " + str(error)) log_error("Failed to create ENet client: " + str(error))
return false return false
multiplayer.multiplayer_peer = peer multiplayer.multiplayer_peer = peer
is_hosting = false is_hosting = false
print("Attempting to connect to ", address, ":", port) log_print("Attempting to connect to " + address + ":" + str(port))
return true
func refresh_webrtc_servers(room_override: String = "") -> bool:
# Recreate Matchbox client to refresh WebRTC signaling connection
if network_mode != 1:
return false
var target_room = room_override if not room_override.is_empty() else room_id
if target_room.is_empty():
log_error("NetworkManager: Cannot refresh WebRTC servers - no room ID")
return false
# Tear down existing client
if matchbox_client:
matchbox_client.disconnect_from_room()
matchbox_client.queue_free()
matchbox_client = null
# Recreate Matchbox client
var matchbox_script = load("res://scripts/matchbox_client.gd")
matchbox_client = Node.new()
matchbox_client.set_script(matchbox_script)
add_child(matchbox_client)
matchbox_client.peer_joined.connect(_on_matchbox_peer_joined)
matchbox_client.peer_left.connect(_on_matchbox_peer_left)
matchbox_client.peer_connected.connect(_on_matchbox_peer_connected)
matchbox_client.connection_succeeded.connect(_on_matchbox_connected)
matchbox_client.connection_failed.connect(_on_matchbox_connection_failed)
matchbox_client.webrtc_ready.connect(_on_matchbox_webrtc_ready)
is_hosting = false
room_id = target_room
log_print("NetworkManager: Refreshing WebRTC servers for room ID: " + room_id)
if not matchbox_client.connect_to_room(room_id, false):
log_error("NetworkManager: Failed to reconnect to Matchbox room during refresh")
return false
return true return true
func disconnect_from_game(): func disconnect_from_game():
# Unregister room from registry
if room_registry:
room_registry.unregister_room()
if matchbox_client: if matchbox_client:
matchbox_client.disconnect_from_room() matchbox_client.disconnect_from_room()
matchbox_client.queue_free() matchbox_client.queue_free()
@@ -265,16 +327,49 @@ func set_local_player_count(count: int):
func _generate_player_names(count: int, peer_id: int) -> Array: func _generate_player_names(count: int, peer_id: int) -> Array:
var names = [] var names = []
for i in range(count): for i in range(count):
if count == 1:
# Only one player - don't add index suffix
names.append("Player%d" % peer_id)
else:
# Multiple players - add index
names.append("Player%d_%d" % [peer_id, i + 1]) names.append("Player%d_%d" % [peer_id, i + 1])
return names return names
# Called when a peer connects to the server # Called when a peer connects to the server
func _on_peer_connected(id: int): func _on_peer_connected(id: int):
print("Peer connected: ", id) log_print("Peer connected: " + str(id))
# Peer is now actually available for RPCs - emit player_connected signal
# Make sure player info is registered (should have been done in _on_matchbox_peer_connected)
if not players_info.has(id):
# Fallback: register default player info if not already registered
players_info[id] = {
"local_player_count": 1, # Default, will be updated via RPC
"player_names": _generate_player_names(1, id)
}
# For joiners, emit connection_succeeded when the first peer (host) connects
# This ensures the connection is actually established and RPCs can be received
# Note: On web, multiplayer.peer_connected might not fire automatically,
# so we also check in _on_matchbox_peer_connected as a fallback
if not is_hosting and id == 1: # Host connected to joiner
log_print("NetworkManager: Joiner - host connected via multiplayer.peer_connected, emitting connection_succeeded")
connection_succeeded.emit()
# Emit player connected signal (peer is now available for RPCs)
player_connected.emit(id, players_info[id])
# Update room registry if hosting (player count changed)
if is_hosting and room_registry and not room_id.is_empty():
var player_count = get_all_player_ids().size()
var _level = 1
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
_level = game_world.current_level
room_registry.send_room_update(player_count)
# Called when a peer disconnects # Called when a peer disconnects
func _on_peer_disconnected(id: int): func _on_peer_disconnected(id: int):
print("Peer disconnected: ", id) log_print("Peer disconnected: " + str(id))
# Get player_info before erasing it # Get player_info before erasing it
var player_info = {} var player_info = {}
if players_info.has(id): if players_info.has(id):
@@ -282,24 +377,68 @@ func _on_peer_disconnected(id: int):
players_info.erase(id) players_info.erase(id)
player_disconnected.emit(id, player_info) player_disconnected.emit(id, player_info)
# Update room registry if hosting (player count changed)
if is_hosting and room_registry and not room_id.is_empty():
var player_count = get_all_player_ids().size()
var _level = 1
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
_level = game_world.current_level
room_registry.send_room_update(player_count)
# Called on client when successfully connected to server # Called on client when successfully connected to server
func _on_connected_to_server(): func _on_connected_to_server():
print("Successfully connected to server") log_print("Successfully connected to server")
connection_succeeded.emit() connection_succeeded.emit()
# Send our player info to the server # Send our player info to the server
var my_id = multiplayer.get_unique_id() var my_id = multiplayer.get_unique_id()
_register_player.rpc_id(1, my_id, local_player_count) _register_player.rpc_id(1, my_id, local_player_count)
# Update room registry if hosting (player count changed)
if is_hosting and room_registry and not room_id.is_empty():
var player_count = get_all_player_ids().size()
var _level = 1
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
_level = game_world.current_level
room_registry.send_room_update(player_count)
# Called on client when connection fails # Called on client when connection fails
func _on_connection_failed(): func _on_connection_failed():
print("Connection failed") log_print("Connection failed")
multiplayer.multiplayer_peer = null multiplayer.multiplayer_peer = null
connection_failed.emit() connection_failed.emit()
# Called on client when disconnected from server # Called on client when disconnected from server
func _on_server_disconnected(): func _on_server_disconnected():
print("Server disconnected") log_print("Server disconnected")
# Store reconnection info if we're a joiner (not host)
if not is_hosting and not room_id.is_empty():
reconnection_room_id = room_id
# Get current level from game_world
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has("current_level"):
reconnection_level = game_world.current_level
log_print("NetworkManager: Stored reconnection info - room_id: " + reconnection_room_id + ", level: " + str(reconnection_level))
else:
reconnection_level = 0 # Unknown level
# Add local-only chat message "Connection lost..."
if game_world:
var chat_ui = game_world.get_node_or_null("ChatUI")
if chat_ui and chat_ui.has_method("add_local_message"):
chat_ui.add_local_message("System", "Connection lost...")
# Start reconnection attempt
reconnection_attempting = true
reconnection_timer = RECONNECTION_DELAY
log_print("NetworkManager: Will attempt to reconnect in " + str(RECONNECTION_DELAY) + " seconds")
# Emit connection_failed signal to show UI message
connection_failed.emit()
multiplayer.multiplayer_peer = null multiplayer.multiplayer_peer = null
players_info.clear() players_info.clear()
@@ -318,7 +457,8 @@ func _register_player(peer_id: int, local_count: int):
print("NetworkManager: Total players_info: ", players_info) print("NetworkManager: Total players_info: ", players_info)
# Sync all player info to the new client (so they know about everyone) # Sync all player info to the new client (so they know about everyone)
_sync_players.rpc_id(peer_id, players_info) # Defer RPC to ensure the peer is fully registered in the multiplayer system
call_deferred("_sync_players_to_peer", peer_id, players_info)
# Notify all clients (including the new one) about the new player # Notify all clients (including the new one) about the new player
_notify_player_joined.rpc(peer_id, players_info[peer_id]) _notify_player_joined.rpc(peer_id, players_info[peer_id])
@@ -326,6 +466,11 @@ func _register_player(peer_id: int, local_count: int):
# Emit signal on server # Emit signal on server
player_connected.emit(peer_id, players_info[peer_id]) player_connected.emit(peer_id, players_info[peer_id])
func _sync_players_to_peer(peer_id: int, all_players_info: Dictionary):
# Helper function to send _sync_players RPC (called via call_deferred)
if multiplayer.is_server():
_sync_players.rpc_id(peer_id, all_players_info)
# RPC to sync all player info to a newly connected client # RPC to sync all player info to a newly connected client
@rpc("authority", "reliable") @rpc("authority", "reliable")
func _sync_players(all_players_info: Dictionary): func _sync_players(all_players_info: Dictionary):
@@ -353,32 +498,58 @@ func get_player_info(peer_id: int) -> Dictionary:
return players_info.get(peer_id, {}) return players_info.get(peer_id, {})
# Matchbox callback handlers # Matchbox callback handlers
func _on_matchbox_connected(): func _on_matchbox_connected(was_reconnecting: bool = false):
print("NetworkManager: Connected to Matchbox server") log_print("NetworkManager: Connected to Matchbox server")
# WebRTC peer will be set up when Welcome message is received with our peer ID # WebRTC peer will be set up when Welcome message is received with our peer ID
# If host was reconnecting, show colorful reconnection message
if is_hosting and was_reconnecting:
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
var chat_ui = game_world.get_node_or_null("ChatUI")
if chat_ui and chat_ui.has_method("add_colorful_local_message"):
chat_ui.add_colorful_local_message("System", "Matchbox connection re-established!")
func _on_matchbox_webrtc_ready(): func _on_matchbox_webrtc_ready():
print("NetworkManager: WebRTC mesh is ready") log_print("NetworkManager: WebRTC mesh is ready")
# Register ourselves if we're not hosting (clients need to register) # Register room with room registry if hosting
if not is_hosting and matchbox_client: if is_hosting and room_registry and not room_id.is_empty():
var my_peer_id = matchbox_client.get_my_peer_id() var player_count = get_all_player_ids().size()
if my_peer_id > 0 and not players_info.has(my_peer_id): var level = 1
players_info[my_peer_id] = { var game_world = get_tree().get_first_node_in_group("game_world")
"local_player_count": local_player_count, if game_world:
"player_names": _generate_player_names(local_player_count, my_peer_id) level = game_world.current_level
} room_registry.register_room(room_id, player_count, level)
print("NetworkManager: Registered joining player with peer ID: ", my_peer_id) log_print("NetworkManager: Registered room " + room_id + " with registry")
# Emit connection_succeeded signal so the game can start # Host can start immediately
connection_succeeded.emit() connection_succeeded.emit()
else:
# For joiners, wait for peer_connected to be emitted before starting
# This ensures the connection is actually established and RPCs can be received
log_print("NetworkManager: Joiner - waiting for peer connection before starting game")
# connection_succeeded will be emitted in _on_peer_connected when the first peer (host) connects
func _on_matchbox_connection_failed(): func _on_matchbox_connection_failed():
print("NetworkManager: Failed to connect to Matchbox server") log_print("NetworkManager: Failed to connect to Matchbox server")
# If hosting, add local chat message and show UI
if is_hosting:
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
var chat_ui = game_world.get_node_or_null("ChatUI")
if chat_ui and chat_ui.has_method("add_local_message"):
chat_ui.add_local_message("System", "Lost connection to matchbox server...")
# Emit connection_failed to show UI message
connection_failed.emit()
else:
# For joiners, just emit the signal (they already have their own handling)
connection_failed.emit() connection_failed.emit()
func _on_matchbox_peer_joined(peer_id: int): func _on_matchbox_peer_joined(peer_id: int):
print("NetworkManager: Matchbox peer joined: ", peer_id) log_print("NetworkManager: Matchbox peer joined: " + str(peer_id))
# Peer connection will be created by Matchbox client # Peer connection will be created by Matchbox client
# Once connected, we'll add it to WebRTC mesh # Once connected, we'll add it to WebRTC mesh
@@ -388,23 +559,160 @@ func _on_matchbox_peer_left(peer_id: int):
func _on_matchbox_peer_connected(peer_id: int): func _on_matchbox_peer_connected(peer_id: int):
print("NetworkManager: Matchbox peer connected: ", peer_id) print("NetworkManager: Matchbox peer connected: ", peer_id)
# Add peer to WebRTC mesh # Note: Peer connection is already added to WebRTC mesh in _create_peer_connection()
if matchbox_client: # This signal is emitted after the answer is received, but the peer might not be available for RPCs yet
matchbox_client.add_peer_to_mesh(peer_id) # We'll emit player_connected from _on_peer_connected() when the peer is actually available
# Register player info # Register player info (we'll emit player_connected when multiplayer.peer_connected fires)
if not players_info.has(peer_id): if not players_info.has(peer_id):
players_info[peer_id] = { players_info[peer_id] = {
"local_player_count": 1, # Default, will be updated via RPC "local_player_count": 1, # Default, will be updated via RPC
"player_names": _generate_player_names(1, peer_id) "player_names": _generate_player_names(1, peer_id)
} }
# Emit player connected signal # For joiners, if this is the host (peer_id 1) and connection_succeeded hasn't been emitted yet,
player_connected.emit(peer_id, players_info[peer_id]) # wait a bit and then emit it as a fallback (in case multiplayer.peer_connected doesn't fire on web)
if not is_hosting and peer_id == 1:
# Check if we've already set up a timer for this peer (prevent multiple timers)
if has_meta("connection_succeeded_timer_set"):
log_print("NetworkManager: Joiner - timer already set for peer " + str(peer_id) + ", skipping")
return
set_meta("connection_succeeded_timer_set", true)
log_print("NetworkManager: Joiner - host connected via Matchbox, waiting for multiplayer.peer_connected...")
# Store reference to scene tree before timer
var scene_tree = get_tree()
if not scene_tree:
# Node is not in scene tree, emit immediately
log_print("NetworkManager: Joiner - node not in scene tree, emitting connection_succeeded immediately")
connection_succeeded.emit()
return
# Wait a short time to see if multiplayer.peer_connected fires
# If it doesn't fire within 2 seconds, emit connection_succeeded anyway
var timer = scene_tree.create_timer(2.0)
timer.timeout.connect(func():
# Check if connection_succeeded was already emitted (by checking if we're in game scene)
if not is_inside_tree():
log_print("NetworkManager: Joiner - node no longer in tree, skipping fallback emit")
return
var current_tree = get_tree()
if not current_tree:
log_print("NetworkManager: Joiner - no scene tree, skipping fallback emit")
return
var game_world = current_tree.get_first_node_in_group("game_world")
if not game_world:
# Still in main menu, so connection_succeeded wasn't emitted
log_print("NetworkManager: Joiner - multiplayer.peer_connected didn't fire, emitting connection_succeeded as fallback")
# Emit signal immediately - NetworkManager is an autoload so it should always be in tree
log_print("NetworkManager: is_inside_tree() = " + str(is_inside_tree()) + ", emitting connection_succeeded signal (fallback)")
connection_succeeded.emit()
log_print("NetworkManager: connection_succeeded signal emitted")
# Also try to get GameUI and call _start_game directly as a fallback
# Search for GameUI by script (it has game_ui.gd script)
var game_ui = null
var root = current_tree.get_root()
# Try different possible paths (GameUI is at /root/GameUI)
var possible_paths = [
"/root/GameUI",
"GameUI",
"Main/GameUI",
"/root/Main/GameUI"
]
for path in possible_paths:
var node = root.get_node_or_null(path)
if node and node.get_script() and node.get_script().resource_path.ends_with("game_ui.gd"):
game_ui = node
log_print("NetworkManager: Found GameUI at path: " + path)
break
# If not found by path, search all nodes recursively
if not game_ui:
game_ui = _find_node_by_script(root, "game_ui.gd")
if game_ui:
log_print("NetworkManager: Found GameUI by recursive script search: " + str(game_ui.get_path()))
if game_ui and game_ui.has_method("_start_game"):
log_print("NetworkManager: Found GameUI node, calling _start_game directly as fallback")
game_ui.call_deferred("_start_game")
else:
log_print("NetworkManager: GameUI node not found or doesn't have _start_game method")
# Try to change scene directly as last resort
var game_world_scene = load("res://scenes/game_world.tscn")
if game_world_scene:
log_print("NetworkManager: Attempting to change scene directly to game_world.tscn")
current_tree.change_scene_to_packed(game_world_scene)
else:
log_print("NetworkManager: Joiner - already in game scene, connection_succeeded was already emitted")
)
# Don't emit player_connected here - wait for multiplayer.peer_connected signal
# which fires when the peer is actually available for RPCs
func _emit_connection_succeeded_safe():
"""Safely emit connection_succeeded signal - checks if node is still valid"""
if is_inside_tree():
log_print("NetworkManager: Emitting connection_succeeded signal (fallback)")
connection_succeeded.emit()
else:
log_print("NetworkManager: Cannot emit connection_succeeded - node not in tree")
func _find_node_by_script(node: Node, script_name: String) -> Node:
"""Recursively search for a node with a specific script"""
if node.get_script() and node.get_script().resource_path.ends_with(script_name):
return node
for child in node.get_children():
var result = _find_node_by_script(child, script_name)
if result:
return result
return null
func get_room_id() -> String: func get_room_id() -> String:
return room_id return room_id
func _process(delta: float):
# Handle reconnection timer
if reconnection_attempting:
reconnection_timer -= delta
if reconnection_timer <= 0.0:
reconnection_attempting = false
_attempt_reconnect()
func _attempt_reconnect():
# Only reconnect if we're a joiner and have reconnection info
if is_hosting or reconnection_room_id.is_empty():
log_print("NetworkManager: Cannot reconnect - is_hosting: " + str(is_hosting) + ", reconnection_room_id: " + reconnection_room_id)
return
log_print("NetworkManager: Attempting to reconnect to room: " + reconnection_room_id)
# Attempt to reconnect using the stored room_id
var success = join_game(reconnection_room_id)
if not success:
log_error("NetworkManager: Reconnection attempt failed, will retry")
# Retry after delay
reconnection_attempting = true
reconnection_timer = RECONNECTION_DELAY
func fetch_available_rooms() -> bool:
"""Fetch available rooms from the registry"""
if room_registry:
return room_registry.fetch_available_rooms()
return false
func _on_rooms_fetched(rooms: Array):
"""Callback when rooms are fetched from registry"""
log_print("NetworkManager: Received " + str(rooms.size()) + " available rooms")
# Forward the signal to listeners
rooms_fetched.emit(rooms)
func get_local_ip() -> String: func get_local_ip() -> String:
var addresses = IP.get_local_addresses() var addresses = IP.get_local_addresses()
for addr in addresses: for addr in addresses:

View File

@@ -263,8 +263,6 @@ func _ready():
_setup_player_appearance() _setup_player_appearance()
# Authority is set by player_manager after adding to scene # Authority is set by player_manager after adding to scene
# Just log it here
print("Player ", name, " ready. Authority: ", get_multiplayer_authority(), " Is local: ", is_local_player)
# Hide interaction indicator by default # Hide interaction indicator by default
if interaction_indicator: if interaction_indicator:
@@ -385,7 +383,6 @@ func _initialize_character_stats():
appearance_rng = RandomNumberGenerator.new() appearance_rng = RandomNumberGenerator.new()
var seed_value = hash(str(peer_id) + "_" + str(local_player_index)) var seed_value = hash(str(peer_id) + "_" + str(local_player_index))
appearance_rng.seed = seed_value appearance_rng.seed = seed_value
print(name, " appearance/stats seed: ", seed_value, " (peer_id: ", peer_id, ", local_index: ", local_player_index, ")")
# Create character stats # Create character stats
character_stats = CharacterStats.new() character_stats = CharacterStats.new()
@@ -415,13 +412,7 @@ func _randomize_stats():
character_stats.baseStats.cha = appearance_rng.randi_range(8, 12) character_stats.baseStats.cha = appearance_rng.randi_range(8, 12)
character_stats.baseStats.lck = appearance_rng.randi_range(8, 12) character_stats.baseStats.lck = appearance_rng.randi_range(8, 12)
print(name, " randomized stats: STR=", character_stats.baseStats.str, # Stats randomized (verbose logging removed)
" DEX=", character_stats.baseStats.dex,
" INT=", character_stats.baseStats.int,
" END=", character_stats.baseStats.end,
" WIS=", character_stats.baseStats.wis,
" CHA=", character_stats.baseStats.cha,
" LCK=", character_stats.baseStats.lck)
func _setup_player_appearance(): func _setup_player_appearance():
# Randomize appearance - players spawn "bare" (naked, no equipment) # Randomize appearance - players spawn "bare" (naked, no equipment)
@@ -600,10 +591,7 @@ func _apply_appearance_to_sprites():
if sprite_weapon: if sprite_weapon:
sprite_weapon.texture = null # Weapons don't use character sprite layers sprite_weapon.texture = null # Weapons don't use character sprite layers
print(name, " appearance applied: skin=", character_stats.skin, # Appearance applied (verbose logging removed)
" hair=", character_stats.hairstyle,
" facial_hair=", character_stats.facial_hair,
" eyes=", character_stats.eyes)
func _on_character_changed(_char: CharacterStats): func _on_character_changed(_char: CharacterStats):
# Update appearance when character stats change (e.g., equipment) # Update appearance when character stats change (e.g., equipment)
@@ -619,7 +607,7 @@ func _on_character_changed(_char: CharacterStats):
equipment_data[slot_name] = item.save() # Serialize item data equipment_data[slot_name] = item.save() # Serialize item data
else: else:
equipment_data[slot_name] = null equipment_data[slot_name] = null
_sync_equipment.rpc(equipment_data) _rpc_to_ready_peers("_sync_equipment", [equipment_data])
# Sync equipment and inventory to client (when server adds/removes items for a client player) # Sync equipment and inventory to client (when server adds/removes items for a client player)
# This ensures joiners see items they pick up and equipment changes # This ensures joiners see items they pick up and equipment changes
@@ -661,6 +649,88 @@ func _is_player(obj) -> bool:
# Check if it's a player by looking for player-specific properties # Check if it's a player by looking for player-specific properties
return obj.is_in_group("player") or ("is_local_player" in obj and "peer_id" in obj) return obj.is_in_group("player") or ("is_local_player" in obj and "peer_id" in obj)
# Helper function to get consistent object name for network sync
func _get_object_name_for_sync(obj) -> String:
# For interactable objects, use the consistent name (InteractableObject_X)
if obj.name.begins_with("InteractableObject_"):
return obj.name
if obj.has_meta("object_index"):
var obj_index = obj.get_meta("object_index")
return "InteractableObject_%d" % obj_index
# For players, use their unique name
if _is_player(obj):
return obj.name
# Last resort: use the node name (might be auto-generated like @CharacterBody2D@82)
return obj.name
func _get_log_prefix() -> String:
if multiplayer.has_multiplayer_peer():
return "[H] " if multiplayer.is_server() else "[J] "
return ""
func _can_place_down_at(place_pos: Vector2, placed_obj: Node) -> bool:
var space_state = get_world_2d().direct_space_state
var placed_shape = _get_collision_shape_for(placed_obj)
if not placed_shape:
# Fallback to 16x16
placed_shape = RectangleShape2D.new()
placed_shape.size = Vector2(16, 16)
var params = PhysicsShapeQueryParameters2D.new()
params.shape = placed_shape
params.transform = Transform2D(0.0, place_pos)
params.collision_mask = 1 | 2 | 64 # Players, objects, walls
params.exclude = [self, placed_obj]
var hits = space_state.intersect_shape(params, 8)
return hits.is_empty()
func _find_closest_place_pos(direction: Vector2, placed_obj: Node) -> Vector2:
var dir = direction.normalized()
if dir.length() < 0.1:
dir = last_movement_direction.normalized()
if dir.length() < 0.1:
dir = Vector2.RIGHT
var player_extent = _get_collision_extent(self)
var obj_extent = _get_collision_extent(placed_obj)
# Start just outside player + object bounds
var start_dist = max(8.0, player_extent + obj_extent + 1.0)
var max_dist = start_dist + 32.0
var step = 2.0
var best_pos = global_position + dir * max_dist
for d in range(int(start_dist), int(max_dist) + 1, int(step)):
var test_pos = global_position + dir * float(d)
if _can_place_down_at(test_pos, placed_obj):
return test_pos
return best_pos
func _get_collision_shape_for(node: Node) -> Shape2D:
if not node:
return null
var shape_node = node.get_node_or_null("CollisionShape2D")
if not shape_node:
shape_node = node.find_child("CollisionShape2D", true, false)
if shape_node and "shape" in shape_node:
return shape_node.shape
return null
func _get_collision_extent(node: Node) -> float:
var shape = _get_collision_shape_for(node)
if shape is RectangleShape2D:
return max(shape.size.x, shape.size.y) * 0.5
if shape is CapsuleShape2D:
return shape.radius + shape.height * 0.5
if shape is CircleShape2D:
return shape.radius
if shape is ConvexPolygonShape2D:
var rect = shape.get_rect()
return max(rect.size.x, rect.size.y) * 0.5
# Fallback
return 8.0
func _update_animation(delta): func _update_animation(delta):
# Update animation frame timing # Update animation frame timing
time_since_last_frame += delta time_since_last_frame += delta
@@ -1031,15 +1101,10 @@ func _physics_process(delta):
else: else:
print("Player ", name, " (server) - all clients now ready! (no ready times tracked)") print("Player ", name, " (server) - all clients now ready! (no ready times tracked)")
# On server, also wait a bit after setting all_clients_ready to ensure nodes are registered # Sync position to all ready peers (clients and server)
if not multiplayer.is_server(): # Only send if node is still valid and in tree
_sync_position.rpc(position, velocity, position_z, is_airborne, current_direction, current_animation) if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree() and is_instance_valid(self):
elif all_clients_ready: _rpc_to_ready_peers("_sync_position", [position, velocity, position_z, is_airborne, current_direction, current_animation])
# Wait an additional 0.2 seconds after setting all_clients_ready before sending RPCs
var current_time = Time.get_ticks_msec() / 1000.0
var time_since_ready = current_time - all_clients_ready_time
if time_since_ready >= 0.2:
_sync_position.rpc(position, velocity, position_z, is_airborne, current_direction, current_animation)
# Always move and slide to maintain horizontal velocity # Always move and slide to maintain horizontal velocity
# When airborne, velocity is set by throw and decreases with friction # When airborne, velocity is set by throw and decreases with friction
@@ -1396,7 +1461,11 @@ func _handle_interactions():
# Gamepad (X button) # Gamepad (X button)
attack_just_pressed = Input.is_joy_button_pressed(input_device, JOY_BUTTON_X) attack_just_pressed = Input.is_joy_button_pressed(input_device, JOY_BUTTON_X)
if attack_just_pressed and can_attack and not is_lifting and not is_pushing: if attack_just_pressed and can_attack:
if is_lifting:
# Attack while lifting -> throw immediately (no movement required)
_force_throw_held_object(last_movement_direction)
elif not is_pushing:
_perform_attack() _perform_attack()
# Note: just_grabbed_this_frame is reset when we block a release, or stays true if we grabbed this frame # Note: just_grabbed_this_frame is reset when we block a release, or stays true if we grabbed this frame
@@ -1475,9 +1544,11 @@ func _try_grab():
# Sync initial grab to network # Sync initial grab to network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_initial_grab.rpc(held_object.get_path(), grab_offset) # Use consistent object name or index instead of path
var obj_name = _get_object_name_for_sync(held_object)
_rpc_to_ready_peers("_sync_initial_grab", [obj_name, grab_offset])
# Sync the grab state # Sync the grab state
_sync_grab.rpc(held_object.get_path(), is_lifting, push_axis) _rpc_to_ready_peers("_sync_grab", [obj_name, is_lifting, push_axis])
print("Grabbed: ", closest_body.name) print("Grabbed: ", closest_body.name)
@@ -1525,7 +1596,8 @@ func _lift_object():
# Sync to network (non-blocking) # Sync to network (non-blocking)
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_grab.rpc(held_object.get_path(), true, push_axis) var obj_name = _get_object_name_for_sync(held_object)
_rpc_to_ready_peers("_sync_grab", [obj_name, true, push_axis])
print("Lifted: ", held_object.name) print("Lifted: ", held_object.name)
$SfxLift.play() $SfxLift.play()
@@ -1565,7 +1637,8 @@ func _start_pushing():
# Sync push state to network # Sync push state to network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_grab.rpc(held_object.get_path(), false, push_axis) # false = pushing, not lifting var obj_name = _get_object_name_for_sync(held_object)
_rpc_to_ready_peers("_sync_grab", [obj_name, false, push_axis]) # false = pushing, not lifting
print("Pushing: ", held_object.name, " along axis: ", push_axis, " facing dir: ", push_direction_locked) print("Pushing: ", held_object.name, " along axis: ", push_axis, " facing dir: ", push_direction_locked)
@@ -1598,7 +1671,8 @@ func _stop_pushing():
# Sync release to network # Sync release to network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_release.rpc(released_obj.get_path()) var obj_name = _get_object_name_for_sync(released_obj)
_rpc_to_ready_peers("_sync_release", [obj_name])
# Release the object and re-enable collision completely # Release the object and re-enable collision completely
if _is_box(released_obj): if _is_box(released_obj):
@@ -1660,6 +1734,99 @@ func _throw_object():
is_lifting = false is_lifting = false
is_pushing = false is_pushing = false
# Re-enable collision completely
if _is_box(thrown_obj):
# Box: set position and physics first
thrown_obj.global_position = throw_start_pos
# Set throw velocity for box (same force as player throw)
if "throw_velocity" in thrown_obj:
thrown_obj.throw_velocity = throw_direction * throw_force / thrown_obj.weight
if "is_frozen" in thrown_obj:
thrown_obj.is_frozen = false
# Make box airborne with same arc as players
if "is_airborne" in thrown_obj:
thrown_obj.is_airborne = true
thrown_obj.position_z = 2.5
thrown_obj.velocity_z = 100.0 # Scaled down for 1x scale
# Call on_thrown if available
if thrown_obj.has_method("on_thrown"):
thrown_obj.on_thrown(self, throw_direction * throw_force)
# ⚡ Delay collision re-enable to prevent self-collision
await get_tree().create_timer(0.1).timeout
if thrown_obj and is_instance_valid(thrown_obj):
thrown_obj.set_collision_layer_value(2, true)
thrown_obj.set_collision_mask_value(1, true)
thrown_obj.set_collision_mask_value(2, true)
thrown_obj.set_collision_mask_value(7, true)
elif _is_player(thrown_obj):
# Player: set position and physics first
thrown_obj.global_position = throw_start_pos
# Set horizontal velocity for the arc
thrown_obj.velocity = throw_direction * throw_force * 0.8 # Slightly reduced for arc
# Make player airborne with Z velocity
if "is_airborne" in thrown_obj:
thrown_obj.is_airborne = true
thrown_obj.position_z = 2.5 # Start slightly off ground
thrown_obj.velocity_z = 100.0 # Scaled down for 1x scale
if thrown_obj.has_method("set_being_held"):
thrown_obj.set_being_held(false)
# ⚡ Delay collision re-enable to prevent self-collision
await get_tree().create_timer(0.1).timeout
if thrown_obj and is_instance_valid(thrown_obj):
thrown_obj.set_collision_layer_value(1, true)
thrown_obj.set_collision_mask_value(1, true)
thrown_obj.set_collision_mask_value(7, true)
if thrown_obj.has_method("on_thrown"):
thrown_obj.on_thrown(self, throw_direction * throw_force)
# Play throw animation
_set_animation("THROW")
$SfxThrow.play()
# Sync throw over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
var obj_name = _get_object_name_for_sync(thrown_obj)
_rpc_to_ready_peers("_sync_throw", [obj_name, throw_start_pos, throw_direction * throw_force, name])
print("Threw ", thrown_obj.name, " from ", throw_start_pos, " with force: ", throw_direction * throw_force)
func _force_throw_held_object(direction: Vector2):
if not held_object or not is_lifting:
return
# Check if object can be thrown
if held_object.has_method("can_be_thrown") and not held_object.can_be_thrown():
# Can't throw this object, place it down instead
_place_down_object()
return
var throw_direction = direction.normalized()
if throw_direction.length() < 0.1:
throw_direction = last_movement_direction.normalized()
if throw_direction.length() < 0.1:
throw_direction = Vector2.RIGHT
# Position object at player's position before throwing
var throw_start_pos = global_position + throw_direction * 10 # Start slightly in front
# Store reference before clearing
var thrown_obj = held_object
# Clear state first (important!)
held_object = null
grab_offset = Vector2.ZERO
is_lifting = false
is_pushing = false
# Re-enable collision completely # Re-enable collision completely
if _is_box(thrown_obj): if _is_box(thrown_obj):
# Box: set position and physics first # Box: set position and physics first
@@ -1718,7 +1885,8 @@ func _throw_object():
# Sync throw over network # Sync throw over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_throw.rpc(thrown_obj.get_path(), throw_start_pos, throw_direction * throw_force, get_path()) var obj_name = _get_object_name_for_sync(thrown_obj)
_rpc_to_ready_peers("_sync_throw", [obj_name, throw_start_pos, throw_direction * throw_force, name])
print("Threw ", thrown_obj.name, " from ", throw_start_pos, " with force: ", throw_direction * throw_force) print("Threw ", thrown_obj.name, " from ", throw_start_pos, " with force: ", throw_direction * throw_force)
@@ -1727,10 +1895,13 @@ func _place_down_object():
return return
# Place object in front of player based on last movement direction # Place object in front of player based on last movement direction
var place_offset = last_movement_direction * 15 # Scaled down for 1x scale var place_pos = _find_closest_place_pos(last_movement_direction, held_object)
var place_pos = global_position + place_offset
var placed_obj = held_object var placed_obj = held_object
if not _can_place_down_at(place_pos, placed_obj):
print("DEBUG: Place down blocked - space not free")
return
# Clear state # Clear state
held_object = null held_object = null
grab_offset = Vector2.ZERO grab_offset = Vector2.ZERO
@@ -1775,7 +1946,8 @@ func _place_down_object():
# Sync place down over network # Sync place down over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_place_down.rpc(placed_obj.get_path(), place_pos) var obj_name = _get_object_name_for_sync(placed_obj)
_rpc_to_ready_peers("_sync_place_down", [obj_name, place_pos])
print("Placed down ", placed_obj.name, " at ", place_pos) print("Placed down ", placed_obj.name, " at ", place_pos)
@@ -1893,7 +2065,7 @@ func _perform_attack():
# Sync attack over network # Sync attack over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_attack.rpc(current_direction, attack_direction) _rpc_to_ready_peers("_sync_attack", [current_direction, attack_direction])
# Reset attack cooldown (instant if cooldown is 0) # Reset attack cooldown (instant if cooldown is 0)
if attack_cooldown > 0: if attack_cooldown > 0:
@@ -1920,7 +2092,8 @@ func _update_lifted_object():
# Sync held object position over network # Sync held object position over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_held_object_pos.rpc(held_object.get_path(), held_object.global_position) var obj_name = _get_object_name_for_sync(held_object)
_rpc_to_ready_peers("_sync_held_object_pos", [obj_name, held_object.global_position])
func _update_pushed_object(): func _update_pushed_object():
if held_object and is_instance_valid(held_object): if held_object and is_instance_valid(held_object):
@@ -1963,10 +2136,10 @@ func _update_pushed_object():
# Account for collision shape offset # Account for collision shape offset
var shape_offset = held_collision_shape.position if held_collision_shape else Vector2.ZERO var shape_offset = held_collision_shape.position if held_collision_shape else Vector2.ZERO
query.transform = Transform2D(0, target_pos + shape_offset) query.transform = Transform2D(0, target_pos + shape_offset)
query.collision_mask = 64 # Layer 7 = walls (bit 6 = 64) query.collision_mask = 1 | 2 | 64 # Players, objects, walls
query.collide_with_areas = false query.collide_with_areas = false
query.collide_with_bodies = true query.collide_with_bodies = true
query.exclude = [held_object.get_rid()] # Exclude the object itself query.exclude = [held_object.get_rid(), get_rid()] # Exclude the object and the holder
var results = space_state.intersect_shape(query) var results = space_state.intersect_shape(query)
was_blocked = results.size() > 0 was_blocked = results.size() > 0
@@ -1976,11 +2149,11 @@ func _update_pushed_object():
# Fallback: use point query # Fallback: use point query
var query = PhysicsPointQueryParameters2D.new() var query = PhysicsPointQueryParameters2D.new()
query.position = target_pos query.position = target_pos
query.collision_mask = 64 # Layer 7 = walls (bit 6 = 64) query.collision_mask = 1 | 2 | 64 # Players, objects, walls
query.collide_with_areas = false query.collide_with_areas = false
query.collide_with_bodies = true query.collide_with_bodies = true
if held_object is CharacterBody2D: if held_object is CharacterBody2D:
query.exclude = [held_object.get_rid()] query.exclude = [held_object.get_rid(), get_rid()]
var results = space_state.intersect_point(query) var results = space_state.intersect_point(query)
was_blocked = results.size() > 0 was_blocked = results.size() > 0
@@ -2001,11 +2174,40 @@ func _update_pushed_object():
# Sync position over network # Sync position over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_held_object_pos.rpc(held_object.get_path(), held_object.global_position) var obj_name = _get_object_name_for_sync(held_object)
_rpc_to_ready_peers("_sync_held_object_pos", [obj_name, held_object.global_position])
# Send RPCs only to peers who are ready to receive them
func _rpc_to_ready_peers(method: String, args: Array = []):
if not multiplayer.has_multiplayer_peer():
return
var game_world = get_tree().get_first_node_in_group("game_world")
# Server can use the ready-peer helper for safe fanout
if multiplayer.is_server() and game_world and game_world.has_method("_rpc_node_to_ready_peers"):
game_world._rpc_node_to_ready_peers(self, method, args)
return
# Clients: only send to peers marked ready by server
if game_world and "clients_ready" in game_world:
for target_peer_id in multiplayer.get_peers():
# Always allow sending to server (peer 1)
if target_peer_id == 1:
callv("rpc_id", [target_peer_id, method] + args)
continue
if game_world.clients_ready.has(target_peer_id) and game_world.clients_ready[target_peer_id]:
callv("rpc_id", [target_peer_id, method] + args)
else:
# Fallback: send to all peers
callv("rpc", [method] + args)
# Network sync # Network sync
@rpc("any_peer", "unreliable") @rpc("any_peer", "unreliable")
func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, airborne: bool = false, dir: int = 0, anim: String = "IDLE"): func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, airborne: bool = false, dir: int = 0, anim: String = "IDLE"):
# Check if node still exists and is valid before processing
if not is_inside_tree() or not is_instance_valid(self):
return
# Only update if we're not the authority (remote player) # Only update if we're not the authority (remote player)
if not is_multiplayer_authority(): if not is_multiplayer_authority():
position = pos position = pos
@@ -2029,6 +2231,10 @@ func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, airborne: bo
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_attack(direction: int, attack_dir: Vector2): func _sync_attack(direction: int, attack_dir: Vector2):
# Sync attack to other clients # Sync attack to other clients
# Check if node still exists and is valid before processing
if not is_inside_tree() or not is_instance_valid(self):
return
if not is_multiplayer_authority(): if not is_multiplayer_authority():
current_direction = direction as Direction current_direction = direction as Direction
_set_animation("SWORD") _set_animation("SWORD")
@@ -2036,6 +2242,10 @@ func _sync_attack(direction: int, attack_dir: Vector2):
# Delay before spawning sword slash # Delay before spawning sword slash
await get_tree().create_timer(0.15).timeout await get_tree().create_timer(0.15).timeout
# Check again after delay - node might have been destroyed
if not is_inside_tree() or not is_instance_valid(self):
return
# Spawn sword projectile on client # Spawn sword projectile on client
if sword_projectile_scene: if sword_projectile_scene:
var projectile = sword_projectile_scene.instantiate() var projectile = sword_projectile_scene.instantiate()
@@ -2048,11 +2258,36 @@ func _sync_attack(direction: int, attack_dir: Vector2):
print(name, " performed synced attack!") print(name, " performed synced attack!")
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_throw(obj_path: NodePath, throw_pos: Vector2, force: Vector2, thrower_path: NodePath): func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_name: String):
# Sync throw to all clients (RPC sender already threw on their side) # Sync throw to all clients (RPC sender already threw on their side)
var obj = get_node_or_null(obj_path) # Check if node is still valid and in tree
var thrower = get_node_or_null(thrower_path) if not is_inside_tree():
print("_sync_throw received: ", obj_path, " found obj: ", obj != null, " thrower: ", thrower != null, " is_authority: ", is_multiplayer_authority()) return
# Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_") and entities_node:
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
var thrower = null
if entities_node:
thrower = entities_node.get_node_or_null(thrower_name)
print("_sync_throw received: ", obj_name, " found obj: ", obj != null, " thrower: ", thrower != null, " is_authority: ", is_multiplayer_authority())
if obj: if obj:
obj.global_position = throw_pos obj.global_position = throw_pos
@@ -2089,6 +2324,7 @@ func _sync_throw(obj_path: NodePath, throw_pos: Vector2, force: Vector2, thrower
obj.set_collision_layer_value(2, true) obj.set_collision_layer_value(2, true)
obj.set_collision_mask_value(1, true) obj.set_collision_mask_value(1, true)
obj.set_collision_mask_value(2, true) obj.set_collision_mask_value(2, true)
obj.set_collision_mask_value(7, true)
elif is_player: elif is_player:
print("Syncing player throw on client! pos: ", throw_pos, " force: ", force) print("Syncing player throw on client! pos: ", throw_pos, " force: ", force)
# Player: set physics first # Player: set physics first
@@ -2109,12 +2345,36 @@ func _sync_throw(obj_path: NodePath, throw_pos: Vector2, force: Vector2, thrower
if obj and is_instance_valid(obj): if obj and is_instance_valid(obj):
obj.set_collision_layer_value(1, true) obj.set_collision_layer_value(1, true)
obj.set_collision_mask_value(1, true) obj.set_collision_mask_value(1, true)
obj.set_collision_mask_value(7, true)
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_initial_grab(obj_path: NodePath, _offset: Vector2): func _sync_initial_grab(obj_name: String, _offset: Vector2):
# Sync initial grab to other clients # Sync initial grab to other clients
# Check if node is still valid and in tree
if not is_inside_tree():
return
if not is_multiplayer_authority(): if not is_multiplayer_authority():
var obj = get_node_or_null(obj_path) # Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_"):
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
if obj: if obj:
# Disable collision for grabbed object # Disable collision for grabbed object
if _is_box(obj): if _is_box(obj):
@@ -2125,13 +2385,36 @@ func _sync_initial_grab(obj_path: NodePath, _offset: Vector2):
obj.set_collision_layer_value(1, false) obj.set_collision_layer_value(1, false)
obj.set_collision_mask_value(1, false) obj.set_collision_mask_value(1, false)
print("Synced initial grab on client: ", obj_path) print("Synced initial grab on client: ", obj_name)
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_grab(obj_path: NodePath, is_lift: bool, axis: Vector2 = Vector2.ZERO): func _sync_grab(obj_name: String, is_lift: bool, axis: Vector2 = Vector2.ZERO):
# Sync lift/push state to other clients # Sync lift/push state to other clients
# Check if node is still valid and in tree
if not is_inside_tree():
return
if not is_multiplayer_authority(): if not is_multiplayer_authority():
var obj = get_node_or_null(obj_path) # Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_"):
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
if obj: if obj:
if is_lift: if is_lift:
# Lifting - completely disable collision # Lifting - completely disable collision
@@ -2174,10 +2457,33 @@ func _sync_grab(obj_path: NodePath, is_lift: bool, axis: Vector2 = Vector2.ZERO)
print("Synced grab on client: lift=", is_lift, " axis=", axis) print("Synced grab on client: lift=", is_lift, " axis=", axis)
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_release(obj_path: NodePath): func _sync_release(obj_name: String):
# Sync release to other clients # Sync release to other clients
# Check if node is still valid and in tree
if not is_inside_tree():
return
if not is_multiplayer_authority(): if not is_multiplayer_authority():
var obj = get_node_or_null(obj_path) # Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_"):
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
if obj: if obj:
# Re-enable collision completely # Re-enable collision completely
if _is_box(obj): if _is_box(obj):
@@ -2198,10 +2504,33 @@ func _sync_release(obj_path: NodePath):
obj.set_being_held(false) obj.set_being_held(false)
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_place_down(obj_path: NodePath, place_pos: Vector2): func _sync_place_down(obj_name: String, place_pos: Vector2):
# Sync placing down to other clients # Sync placing down to other clients
# Check if node is still valid and in tree
if not is_inside_tree():
return
if not is_multiplayer_authority(): if not is_multiplayer_authority():
var obj = get_node_or_null(obj_path) # Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_"):
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
if obj: if obj:
obj.global_position = place_pos obj.global_position = place_pos
@@ -2247,10 +2576,33 @@ func _sync_teleport_position(new_pos: Vector2):
print(name, " teleported to ", new_pos, " (synced from server, is_authority: ", is_multiplayer_authority(), ")") print(name, " teleported to ", new_pos, " (synced from server, is_authority: ", is_multiplayer_authority(), ")")
@rpc("any_peer", "unreliable") @rpc("any_peer", "unreliable")
func _sync_held_object_pos(obj_path: NodePath, pos: Vector2): func _sync_held_object_pos(obj_name: String, pos: Vector2):
# Sync held object position to other clients # Sync held object position to other clients
# Check if node is still valid and in tree
if not is_inside_tree():
return
if not is_multiplayer_authority(): if not is_multiplayer_authority():
var obj = get_node_or_null(obj_path) # Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_"):
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
if obj: if obj:
# Don't update position if object is airborne (being thrown) # Don't update position if object is airborne (being thrown)
if "is_airborne" in obj and obj.is_airborne: if "is_airborne" in obj and obj.is_airborne:
@@ -2313,28 +2665,34 @@ func _break_free_from_holder():
# Sync break free over network # Sync break free over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_break_free.rpc(being_held_by.get_path(), struggle_direction) _rpc_to_ready_peers("_sync_break_free", [being_held_by.name, struggle_direction])
struggle_time = 0.0 struggle_time = 0.0
struggle_direction = Vector2.ZERO struggle_direction = Vector2.ZERO
being_held_by = null being_held_by = null
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_break_free(holder_path: NodePath, direction: Vector2): func _sync_break_free(holder_name: String, direction: Vector2):
var holder = get_node_or_null(holder_path) var holder = null
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
holder = entities_node.get_node_or_null(holder_name)
if holder and holder.has_method("_force_place_down"): if holder and holder.has_method("_force_place_down"):
holder._force_place_down(direction) holder._force_place_down(direction)
func _force_place_down(direction: Vector2): func _force_place_down(direction: Vector2):
# Forced to place down held object in specified direction # Forced to place down held object in specified direction
if held_object and is_lifting: if held_object and is_lifting:
var place_offset = direction.normalized() * 20 var place_pos = _find_closest_place_pos(direction, held_object)
if place_offset.length() < 0.1:
place_offset = last_movement_direction * 20
var place_pos = position + place_offset
var placed_obj = held_object var placed_obj = held_object
if not _can_place_down_at(place_pos, placed_obj):
print("DEBUG: Forced place down blocked - space not free")
return
# Clear state # Clear state
held_object = null held_object = null
grab_offset = Vector2.ZERO grab_offset = Vector2.ZERO
@@ -2414,9 +2772,17 @@ func take_damage(amount: float, attacker_position: Vector2):
_show_damage_number(0.0, attacker_position, false, false, true) # is_dodged = true _show_damage_number(0.0, attacker_position, false, false, true) # is_dodged = true
# Sync dodge visual to other clients # Sync dodge visual to other clients
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_sync_damage.rpc(0.0, attacker_position, false, false, true) # is_dodged = true _rpc_to_ready_peers("_sync_damage", [0.0, attacker_position, false, false, true]) # is_dodged = true
return # No damage taken, exit early return # No damage taken, exit early
# If taking damage while holding something, drop/throw immediately
if held_object:
if is_lifting:
var throw_dir = (global_position - attacker_position).normalized()
_force_throw_held_object(throw_dir)
else:
_stop_pushing()
# If not dodged, apply damage with DEF reduction # If not dodged, apply damage with DEF reduction
var actual_damage = amount var actual_damage = amount
if character_stats: if character_stats:
@@ -2466,7 +2832,7 @@ func take_damage(amount: float, attacker_position: Vector2):
# Sync damage visual effects to other clients (including damage numbers) # Sync damage visual effects to other clients (including damage numbers)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_sync_damage.rpc(actual_damage, attacker_position) _rpc_to_ready_peers("_sync_damage", [actual_damage, attacker_position])
# Check if dead - but wait for damage animation to play first # Check if dead - but wait for damage animation to play first
var health = character_stats.hp if character_stats else current_health var health = character_stats.hp if character_stats else current_health
@@ -2517,7 +2883,8 @@ func _die():
# Sync release to network # Sync release to network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_release.rpc(released_obj.get_path()) var obj_name = _get_object_name_for_sync(released_obj)
_rpc_to_ready_peers("_sync_release", [obj_name])
print(name, " released ", released_obj.name, " on death") print(name, " released ", released_obj.name, " on death")
else: else:
@@ -2548,7 +2915,7 @@ func _die():
# Sync death over network (only authority sends) # Sync death over network (only authority sends)
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_death.rpc() _rpc_to_ready_peers("_sync_death", [])
# Wait for DIE animation to complete (200+200+200+800 = 1400ms = 1.4s) # Wait for DIE animation to complete (200+200+200+800 = 1400ms = 1.4s)
await get_tree().create_timer(1.4).timeout await get_tree().create_timer(1.4).timeout
@@ -2586,7 +2953,7 @@ func _die():
# THEN sync to other clients # THEN sync to other clients
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_force_holder_to_drop.rpc(other_player.get_path()) _rpc_to_ready_peers("_force_holder_to_drop", [other_player.name])
found_holder = true found_holder = true
break break
@@ -2709,17 +3076,22 @@ func _respawn():
# Sync respawn over network (only authority sends) # Sync respawn over network (only authority sends)
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_respawn.rpc(new_respawn_pos) _rpc_to_ready_peers("_sync_respawn", [new_respawn_pos])
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _force_holder_to_drop(holder_path: NodePath): func _force_holder_to_drop(holder_name: String):
# Force a specific player to drop what they're holding # Force a specific player to drop what they're holding
_force_holder_to_drop_local(holder_path) _force_holder_to_drop_local(holder_name)
func _force_holder_to_drop_local(holder_path: NodePath): func _force_holder_to_drop_local(holder_name: String):
# Local function to clear holder's held object # Local function to clear holder's held object
print("_force_holder_to_drop_local called for holder path: ", holder_path) print("_force_holder_to_drop_local called for holder: ", holder_name)
var holder = get_node_or_null(holder_path) var holder = null
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
holder = entities_node.get_node_or_null(holder_name)
if holder and is_instance_valid(holder): if holder and is_instance_valid(holder):
print(" Found holder: ", holder.name, ", their held_object: ", holder.held_object) print(" Found holder: ", holder.name, ", their held_object: ", holder.held_object)
if holder.held_object == self: if holder.held_object == self:
@@ -2864,16 +3236,29 @@ func add_key(amount: int = 1):
keys += amount keys += amount
print(name, " picked up ", amount, " key(s)! Total keys: ", keys) print(name, " picked up ", amount, " key(s)! Total keys: ", keys)
# Sync key count to owning client (server authoritative)
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
var owner_peer_id = get_multiplayer_authority()
if owner_peer_id != 0 and owner_peer_id != multiplayer.get_unique_id():
_sync_keys.rpc_id(owner_peer_id, keys)
func has_key() -> bool: func has_key() -> bool:
return keys > 0 return keys > 0
func use_key(): func use_key():
if keys > 0: if keys > 0:
keys -= 1 keys -= 1
print(name, " used a key! Remaining keys: ", keys) print(_get_log_prefix(), name, " used a key! Remaining keys: ", keys)
return true return true
return false return false
@rpc("any_peer", "reliable")
func _sync_keys(new_key_count: int):
# Sync key count to client
if not is_inside_tree():
return
keys = new_key_count
@rpc("authority", "reliable") @rpc("authority", "reliable")
func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false): func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false):
# Show damage number (red, using dmg_numbers.png font) above player # Show damage number (red, using dmg_numbers.png font) above player

View File

@@ -81,12 +81,9 @@ func spawn_player(peer_id: int, local_index: int):
# Set multiplayer authority AFTER adding to scene # Set multiplayer authority AFTER adding to scene
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
player.set_multiplayer_authority(peer_id) player.set_multiplayer_authority(peer_id)
print("Set authority for player ", unique_id, " to peer ", peer_id)
players[unique_id] = player players[unique_id] = player
print("Spawned player: ", unique_id, " at ", player.position, " (local: ", player.is_local_player, ")")
func despawn_players_for_peer(peer_id: int): func despawn_players_for_peer(peer_id: int):
var to_remove = [] var to_remove = []
for unique_id in players.keys(): for unique_id in players.keys():

View File

@@ -0,0 +1,226 @@
extends Node
# Room Registry Client - Handles HTTP requests to room discovery server
# Allows hosts to register rooms and joiners to discover available rooms
# Logging - use LogManager for categorized logging
func log_print(message: String):
LogManager.log(message, LogManager.CATEGORY_NETWORK)
func log_error(message: String):
LogManager.log_error(message, LogManager.CATEGORY_NETWORK)
signal rooms_fetched(rooms: Array)
signal room_update_sent(success: bool)
const ROOM_LIST_URL = "https://ruinborn-rooms.thefirstboss.com/index.php"
const ROOM_UPDATE_URL = "https://ruinborn-rooms.thefirstboss.com/update.php"
var http_request_list: HTTPRequest = null
var http_request_update: HTTPRequest = null
var update_timer: Timer = null
var current_room: String = ""
var current_level: int = 1
var pending_update: Dictionary = {} # Store pending update (player_count, level) when request is in progress
var is_update_requesting: bool = false # Flag to track if an update request is in progress
var is_list_requesting: bool = false # Flag to track if a room list request is in progress
func _ready():
# Create HTTPRequest nodes for fetching and updating
http_request_list = HTTPRequest.new()
http_request_list.name = "HTTPRequestList"
add_child(http_request_list)
http_request_list.request_completed.connect(_on_room_list_received)
http_request_update = HTTPRequest.new()
http_request_update.name = "HTTPRequestUpdate"
add_child(http_request_update)
http_request_update.request_completed.connect(_on_room_update_received)
# Create timer for periodic updates
update_timer = Timer.new()
update_timer.name = "UpdateTimer"
update_timer.wait_time = 15.0 # Update every 15 seconds
update_timer.timeout.connect(_on_update_timer_timeout)
update_timer.autostart = false
add_child(update_timer)
func fetch_available_rooms() -> bool:
"""Fetch list of available rooms from the server"""
# Check if a request is already in progress
if is_list_requesting or http_request_list.get_http_client_status() == HTTPClient.STATUS_REQUESTING:
log_print("RoomRegistry: Room list request already in progress, skipping duplicate request")
return false
# Mark that we're starting a request
is_list_requesting = true
log_print("RoomRegistry: Fetching available rooms...")
var error = http_request_list.request(ROOM_LIST_URL)
if error != OK:
log_error("RoomRegistry: Failed to request room list: " + str(error))
is_list_requesting = false # Reset flag on error
return false
return true
func register_room(room_code: String, player_count: int, level: int) -> bool:
"""Register a room and start sending keepalive updates"""
current_room = room_code
current_level = level
log_print("RoomRegistry: Registering room: " + room_code + " with " + str(player_count) + " players, level " + str(level))
# Send initial update
send_room_update(player_count)
# Start periodic updates
update_timer.start()
return true
func send_room_update(player_count: int, level: int = -1) -> bool:
"""Send a keepalive update for the current room"""
if current_room.is_empty():
return false
# Get level from game_world if not provided
if level < 0:
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
level = game_world.current_level
else:
level = current_level
else:
current_level = level
# Check if a request is already in progress - if so, queue this update
# HTTPRequest can only handle one request at a time
if is_update_requesting or http_request_update.get_http_client_status() == HTTPClient.STATUS_REQUESTING:
log_print("RoomRegistry: Update request already in progress, queuing update")
pending_update = {
"player_count": player_count,
"level": level
}
return true # Return true to indicate update is queued
# Clear any pending update since we're sending now
pending_update = {}
# Mark that we're starting a request
is_update_requesting = true
var json_data = {
"data": {
"room": current_room,
"players": player_count,
"level": level
}
}
var json_string = JSON.stringify(json_data)
var headers = ["Content-Type: application/json"]
log_print("RoomRegistry: Sending room update: " + json_string)
var error = http_request_update.request(ROOM_UPDATE_URL, headers, HTTPClient.METHOD_POST, json_string)
if error != OK:
log_error("RoomRegistry: Failed to send room update: " + str(error))
is_update_requesting = false # Reset flag on error
return false
return true
func unregister_room():
"""Stop sending updates and clear room registration"""
log_print("RoomRegistry: Unregistering room: " + current_room)
update_timer.stop()
current_room = ""
current_level = 1
func _on_room_list_received(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray):
"""Handle response from room list request"""
# Reset the requesting flag
is_list_requesting = false
if result != HTTPRequest.RESULT_SUCCESS:
log_error("RoomRegistry: Failed to fetch rooms: HTTP result " + str(result))
rooms_fetched.emit([])
return
if response_code != 200:
log_error("RoomRegistry: HTTP error code: " + str(response_code))
rooms_fetched.emit([])
return
var json = JSON.new()
var parse_error = json.parse(body.get_string_from_utf8())
if parse_error != OK:
log_error("RoomRegistry: Failed to parse room list JSON: " + str(parse_error))
rooms_fetched.emit([])
return
var data = json.data
if not data.has("status") or data["status"] != "OK":
log_error("RoomRegistry: Server returned error status")
rooms_fetched.emit([])
return
var rooms = []
if data.has("data") and data["data"] is Array:
for room_data in data["data"]:
if room_data.has("room"):
rooms.append({
"room": room_data["room"],
"players": room_data.get("players", 0),
"level": room_data.get("level", 1)
})
log_print("RoomRegistry: Found " + str(rooms.size()) + " available rooms")
rooms_fetched.emit(rooms)
func _on_room_update_received(result: int, response_code: int, _headers: PackedStringArray, _body: PackedByteArray):
"""Handle response from room update request"""
# Reset the requesting flag
is_update_requesting = false
if result != HTTPRequest.RESULT_SUCCESS:
log_error("RoomRegistry: Failed to send room update: HTTP result " + str(result))
room_update_sent.emit(false)
# Try to send pending update if any
if not pending_update.is_empty():
send_room_update(pending_update.get("player_count", 0), pending_update.get("level", -1))
return
if response_code != 200:
log_error("RoomRegistry: Room update HTTP error code: " + str(response_code))
room_update_sent.emit(false)
# Try to send pending update if any
if not pending_update.is_empty():
send_room_update(pending_update.get("player_count", 0), pending_update.get("level", -1))
return
log_print("RoomRegistry: Room update sent successfully")
room_update_sent.emit(true)
# Send pending update if any
if not pending_update.is_empty():
var queued_player_count = pending_update.get("player_count", 0)
var queued_level = pending_update.get("level", -1)
pending_update = {} # Clear pending update before sending
send_room_update(queued_player_count, queued_level)
func _on_update_timer_timeout():
"""Periodic timer callback to send keepalive updates"""
if current_room.is_empty():
return
# Get current player count from NetworkManager
var network_manager = get_node("/root/NetworkManager")
if not network_manager:
return
var player_count = network_manager.get_all_player_ids().size()
var level = current_level
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
level = game_world.current_level
current_level = level
send_room_update(player_count, level)

View File

@@ -0,0 +1 @@
uid://b165x5g8k5iwd

View File

@@ -45,9 +45,9 @@ func _on_body_exited(body):
func _on_player_entered_room(player: Node): func _on_player_entered_room(player: Node):
# Handle player entering room # Handle player entering room
print("Player ", player.name, " entered room at ", room.x, ", ", room.y) LogManager.log("Player " + str(player.name) + " entered room at " + str(room.x) + ", " + str(room.y), LogManager.CATEGORY_DUNGEON)
print("RoomTrigger: This trigger is for room (", room.x, ", ", room.y, ", ", room.w, "x", room.h, ")") LogManager.log("RoomTrigger: This trigger is for room (" + str(room.x) + ", " + str(room.y) + ", " + str(room.w) + "x" + str(room.h) + ")", LogManager.CATEGORY_DUNGEON)
print("RoomTrigger: Found ", doors_in_room.size(), " doors in this room") LogManager.log("RoomTrigger: Found " + str(doors_in_room.size()) + " doors in this room", LogManager.CATEGORY_DUNGEON)
# Mark room as entered and update debug label # Mark room as entered and update debug label
room_entered = true room_entered = true
@@ -78,10 +78,14 @@ func _on_player_entered_room(player: Node):
if not door_in_this_room: if not door_in_this_room:
# Door is NOT in this room - DO NOT call it! # Door is NOT in this room - DO NOT call it!
print("RoomTrigger: ERROR - Door ", door.name, " is NOT in room (", room.x, ", ", room.y, ")!") var door_room1_x = str(door_room1.x) if door_room1 and not door_room1.is_empty() else "none"
print("RoomTrigger: Door room1: (", door_room1.x if door_room1 else "none", ", ", door_room1.y if door_room1 else "none", ")") var door_room1_y = str(door_room1.y) if door_room1 and not door_room1.is_empty() else "none"
print("RoomTrigger: Door blocking_room: (", door_blocking_room.x if door_blocking_room else "none", ", ", door_blocking_room.y if door_blocking_room else "none", ")") var door_blocking_room_x = str(door_blocking_room.x) if door_blocking_room and not door_blocking_room.is_empty() else "none"
print("RoomTrigger: Removing from this trigger's doors list!") var door_blocking_room_y = str(door_blocking_room.y) if door_blocking_room and not door_blocking_room.is_empty() else "none"
LogManager.log_error("RoomTrigger: ERROR - Door " + str(door.name) + " is NOT in room (" + str(room.x) + ", " + str(room.y) + ")!", LogManager.CATEGORY_DUNGEON)
LogManager.log("RoomTrigger: Door room1: (" + door_room1_x + ", " + door_room1_y + ")", LogManager.CATEGORY_DUNGEON)
LogManager.log("RoomTrigger: Door blocking_room: (" + door_blocking_room_x + ", " + door_blocking_room_y + ")", LogManager.CATEGORY_DUNGEON)
LogManager.log("RoomTrigger: Removing from this trigger's doors list!", LogManager.CATEGORY_DUNGEON)
doors_in_room.erase(door) doors_in_room.erase(door)
if door.room_trigger_area == self: if door.room_trigger_area == self:
door.room_trigger_area = null door.room_trigger_area = null
@@ -96,13 +100,13 @@ func _on_player_entered_room(player: Node):
door._on_room_entered(player) door._on_room_entered(player)
# Spawn enemies if this room has a spawner # Spawn enemies if this room has a spawner
print("RoomTrigger: About to call _spawn_room_enemies()...") LogManager.log("RoomTrigger: About to call _spawn_room_enemies()...", LogManager.CATEGORY_DUNGEON)
_spawn_room_enemies() _spawn_room_enemies()
print("RoomTrigger: Finished _spawn_room_enemies()") LogManager.log("RoomTrigger: Finished _spawn_room_enemies()", LogManager.CATEGORY_DUNGEON)
func _on_player_exited_room(player: Node): func _on_player_exited_room(player: Node):
# Handle player leaving room # Handle player leaving room
print("Player ", player.name, " exited room at ", room.x, ", ", room.y) LogManager.log("Player " + str(player.name) + " exited room at " + str(room.x) + ", " + str(room.y), LogManager.CATEGORY_DUNGEON)
func _find_room_entities(): func _find_room_entities():
# Find all doors, enemies, and switches that belong to this room # Find all doors, enemies, and switches that belong to this room
@@ -118,13 +122,13 @@ func _find_room_entities():
# CRITICAL: Find doors where room1 == THIS room OR blocking_room == THIS room # CRITICAL: Find doors where room1 == THIS room OR blocking_room == THIS room
# Blocking doors are IN the puzzle room (they lead OUT OF this room) # Blocking doors are IN the puzzle room (they lead OUT OF this room)
# When you enter this room, doors IN this room should close (blocking exits) # When you enter this room, doors IN this room should close (blocking exits)
print("RoomTrigger: Finding doors for room (", room.x, ", ", room.y, ", ", room.w, "x", room.h, ")") LogManager.log("RoomTrigger: Finding doors for room (" + str(room.x) + ", " + str(room.y) + ", " + str(room.w) + "x" + str(room.h) + ")", LogManager.CATEGORY_DUNGEON)
var _total_blocking_doors = 0 var _total_blocking_doors = 0
for child in entities_node.get_children(): for child in entities_node.get_children():
if child.is_in_group("blocking_door") or child.name.begins_with("BlockingDoor_"): if child.is_in_group("blocking_door") or child.name.begins_with("BlockingDoor_"):
_total_blocking_doors += 1 _total_blocking_doors += 1
if not is_instance_valid(child): if not is_instance_valid(child):
print("RoomTrigger: Door ", child.name, " is invalid, skipping") LogManager.log("RoomTrigger: Door " + str(child.name) + " is invalid, skipping", LogManager.CATEGORY_DUNGEON)
continue continue
# Check if door is IN this room (room1 == this room OR blocking_room == this room) # Check if door is IN this room (room1 == this room OR blocking_room == this room)
@@ -142,7 +146,7 @@ func _find_room_entities():
door_room1.w == room.w and door_room1.h == room.h) door_room1.w == room.w and door_room1.h == room.h)
if room1_matches: if room1_matches:
door_in_this_room = true door_in_this_room = true
print("RoomTrigger: Door ", child.name, " room1 matches this room (door IN this room)") LogManager.log("RoomTrigger: Door " + str(child.name) + " room1 matches this room (door IN this room)", LogManager.CATEGORY_DUNGEON)
# For blocking doors, also verify blocking_room matches (it should match room1) # For blocking doors, also verify blocking_room matches (it should match room1)
if door_in_this_room and is_blocking_door and door_blocking_room and not door_blocking_room.is_empty(): if door_in_this_room and is_blocking_door and door_blocking_room and not door_blocking_room.is_empty():
@@ -150,22 +154,24 @@ func _find_room_entities():
door_blocking_room.w == room.w and door_blocking_room.h == room.h) door_blocking_room.w == room.w and door_blocking_room.h == room.h)
if not blocking_matches: if not blocking_matches:
# Blocking door's blocking_room doesn't match - this is an error! # Blocking door's blocking_room doesn't match - this is an error!
print("RoomTrigger: ERROR - Blocking door ", child.name, " room1 matches but blocking_room (", door_blocking_room.x, ", ", door_blocking_room.y, ") doesn't match this room (", room.x, ", ", room.y, ")!") LogManager.log_error("RoomTrigger: ERROR - Blocking door " + str(child.name) + " room1 matches but blocking_room (" + str(door_blocking_room.x) + ", " + str(door_blocking_room.y) + ") doesn't match this room (" + str(room.x) + ", " + str(room.y) + ")!", LogManager.CATEGORY_DUNGEON)
door_in_this_room = false # Reject this door door_in_this_room = false # Reject this door
else: else:
print("RoomTrigger: Blocking door ", child.name, " blocking_room also matches this room (verified)") LogManager.log("RoomTrigger: Blocking door " + str(child.name) + " blocking_room also matches this room (verified)", LogManager.CATEGORY_DUNGEON)
# For non-blocking doors or if room1 didn't match, check blocking_room as fallback # For non-blocking doors or if room1 didn't match, check blocking_room as fallback
if not door_in_this_room and door_blocking_room and not door_blocking_room.is_empty(): if not door_in_this_room and door_blocking_room and not door_blocking_room.is_empty():
door_in_this_room = (door_blocking_room.x == room.x and door_blocking_room.y == room.y and \ door_in_this_room = (door_blocking_room.x == room.x and door_blocking_room.y == room.y and \
door_blocking_room.w == room.w and door_blocking_room.h == room.h) door_blocking_room.w == room.w and door_blocking_room.h == room.h)
if door_in_this_room: if door_in_this_room:
print("RoomTrigger: Door ", child.name, " blocking_room matches this room (fallback check)") LogManager.log("RoomTrigger: Door " + str(child.name) + " blocking_room matches this room (fallback check)", LogManager.CATEGORY_DUNGEON)
else: else:
print("RoomTrigger: Door ", child.name, " blocking_room (", door_blocking_room.x if door_blocking_room else "none", ", ", door_blocking_room.y if door_blocking_room else "none", ") doesn't match this room (", room.x, ", ", room.y, ")") var blocking_room_x = str(door_blocking_room.x) if door_blocking_room and not door_blocking_room.is_empty() else "none"
var blocking_room_y = str(door_blocking_room.y) if door_blocking_room and not door_blocking_room.is_empty() else "none"
LogManager.log("RoomTrigger: Door " + str(child.name) + " blocking_room (" + blocking_room_x + ", " + blocking_room_y + ") doesn't match this room (" + str(room.x) + ", " + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
if not door_room1 and not door_blocking_room: if not door_room1 and not door_blocking_room:
print("RoomTrigger: Door ", child.name, " has no room1 or blocking_room set!") LogManager.log("RoomTrigger: Door " + str(child.name) + " has no room1 or blocking_room set!", LogManager.CATEGORY_DUNGEON)
if door_in_this_room: if door_in_this_room:
# This door is IN THIS room (blocks exits from this room) - add it to this trigger # This door is IN THIS room (blocks exits from this room) - add it to this trigger
@@ -178,11 +184,15 @@ func _find_room_entities():
var this_room_str = "(" + str(room.x) + ", " + str(room.y) + ")" var this_room_str = "(" + str(room.x) + ", " + str(room.y) + ")"
# Debug: Print door's room info to understand why it's matching multiple rooms # Debug: Print door's room info to understand why it's matching multiple rooms
print("RoomTrigger: WARNING - Door ", child.name, " already connected to trigger for room ", other_room_str, "!") var door_room1_x_str = str(door_room1.x) if door_room1 and not door_room1.is_empty() else "none"
print("RoomTrigger: Door room1: (", door_room1.x if door_room1 else "none", ", ", door_room1.y if door_room1 else "none", ")") var door_room1_y_str = str(door_room1.y) if door_room1 and not door_room1.is_empty() else "none"
print("RoomTrigger: Door blocking_room: (", door_blocking_room.x if door_blocking_room else "none", ", ", door_blocking_room.y if door_blocking_room else "none", ")") var door_blocking_room_x_str = str(door_blocking_room.x) if door_blocking_room and not door_blocking_room.is_empty() else "none"
print("RoomTrigger: Current trigger room: ", this_room_str) var door_blocking_room_y_str = str(door_blocking_room.y) if door_blocking_room and not door_blocking_room.is_empty() else "none"
print("RoomTrigger: Skipping this door - it belongs to the other trigger!") LogManager.log("RoomTrigger: WARNING - Door " + str(child.name) + " already connected to trigger for room " + other_room_str + "!", LogManager.CATEGORY_DUNGEON)
LogManager.log("RoomTrigger: Door room1: (" + door_room1_x_str + ", " + door_room1_y_str + ")", LogManager.CATEGORY_DUNGEON)
LogManager.log("RoomTrigger: Door blocking_room: (" + door_blocking_room_x_str + ", " + door_blocking_room_y_str + ")", LogManager.CATEGORY_DUNGEON)
LogManager.log("RoomTrigger: Current trigger room: " + this_room_str, LogManager.CATEGORY_DUNGEON)
LogManager.log("RoomTrigger: Skipping this door - it belongs to the other trigger!", LogManager.CATEGORY_DUNGEON)
# Don't add to this trigger if already connected to another trigger # Don't add to this trigger if already connected to another trigger
if child in doors_in_room: if child in doors_in_room:
@@ -193,7 +203,7 @@ func _find_room_entities():
# CRITICAL: Only add if not already in the list (avoid duplicates) # CRITICAL: Only add if not already in the list (avoid duplicates)
if not child in doors_in_room: if not child in doors_in_room:
doors_in_room.append(child) doors_in_room.append(child)
print("RoomTrigger: Added door ", child.name, " that is IN room (", room.x, ", ", room.y, ") to this trigger") LogManager.log("RoomTrigger: Added door " + str(child.name) + " that is IN room (" + str(room.x) + ", " + str(room.y) + ") to this trigger", LogManager.CATEGORY_DUNGEON)
# Set door's room trigger reference (should be null at this point, but set it anyway) # Set door's room trigger reference (should be null at this point, but set it anyway)
if not child.room_trigger_area: if not child.room_trigger_area:
@@ -202,13 +212,13 @@ func _find_room_entities():
# This door is NOT in this room - ensure it's not in this trigger's list # This door is NOT in this room - ensure it's not in this trigger's list
# This prevents doors from being accidentally connected to wrong triggers # This prevents doors from being accidentally connected to wrong triggers
if child in doors_in_room: if child in doors_in_room:
print("RoomTrigger: Removing door ", child.name, " from this trigger - it's not in this room (", room.x, ", ", room.y, ")") LogManager.log("RoomTrigger: Removing door " + str(child.name) + " from this trigger - it's not in this room (" + str(room.x) + ", " + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
doors_in_room.erase(child) doors_in_room.erase(child)
if child.room_trigger_area == self: if child.room_trigger_area == self:
print("RoomTrigger: Disconnecting door ", child.name, " from this trigger - wrong room!") LogManager.log("RoomTrigger: Disconnecting door " + str(child.name) + " from this trigger - wrong room!", LogManager.CATEGORY_DUNGEON)
child.room_trigger_area = null child.room_trigger_area = null
print("RoomTrigger: Found ", doors_in_room.size(), " doors for room (", room.x, ", ", room.y, ")") LogManager.log("RoomTrigger: Found " + str(doors_in_room.size()) + " doors for room (" + str(room.x) + ", " + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
# Find enemies (only if room has enemies - skip for empty rooms to avoid unnecessary work) # Find enemies (only if room has enemies - skip for empty rooms to avoid unnecessary work)
for child in entities_node.get_children(): for child in entities_node.get_children():
@@ -249,18 +259,18 @@ func _find_room_entities():
switch_tile_y >= room_min_y and switch_tile_y < room_max_y: switch_tile_y >= room_min_y and switch_tile_y < room_max_y:
floor_switches_in_room.append(child) floor_switches_in_room.append(child)
print("RoomTrigger: Found ", enemies_in_room.size(), " enemies and ", floor_switches_in_room.size(), " switches for room (", room.x, ", ", room.y, ")") LogManager.log("RoomTrigger: Found " + str(enemies_in_room.size()) + " enemies and " + str(floor_switches_in_room.size()) + " switches for room (" + str(room.x) + ", " + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
# Update debug label after finding all entities (skip if label not created yet) # Update debug label after finding all entities (skip if label not created yet)
if debug_label: if debug_label:
call_deferred("_update_debug_label") call_deferred("_update_debug_label")
func _spawn_room_enemies(): func _spawn_room_enemies():
print("RoomTrigger: ===== _spawn_room_enemies() CALLED for room (", room.x, ", ", room.y, ") =====") LogManager.log("RoomTrigger: ===== _spawn_room_enemies() CALLED for room (" + str(room.x) + ", " + str(room.y) + ") =====", LogManager.CATEGORY_DUNGEON)
# Spawn enemies when player enters room (if room has spawners and not already spawned) # Spawn enemies when player enters room (if room has spawners and not already spawned)
if enemies_spawned: if enemies_spawned:
print("RoomTrigger: Already spawned enemies, skipping...") LogManager.log("RoomTrigger: Already spawned enemies, skipping...", LogManager.CATEGORY_DUNGEON)
return # Already spawned enemies return # Already spawned enemies
# CRITICAL: Remove any existing smoke puffs before spawning new ones # CRITICAL: Remove any existing smoke puffs before spawning new ones
@@ -269,42 +279,42 @@ func _spawn_room_enemies():
# Find enemy spawners for this room # Find enemy spawners for this room
_find_room_spawners() _find_room_spawners()
print("RoomTrigger: ===== Found ", enemy_spawners.size(), " spawners for room (", room.x, ", ", room.y, ") =====") LogManager.log("RoomTrigger: ===== Found " + str(enemy_spawners.size()) + " spawners for room (" + str(room.x) + ", " + str(room.y) + ") =====", LogManager.CATEGORY_DUNGEON)
# Spawn enemies from all spawners in this room (only once) # Spawn enemies from all spawners in this room (only once)
if enemy_spawners.size() > 0: if enemy_spawners.size() > 0:
for spawner in enemy_spawners: for spawner in enemy_spawners:
if not is_instance_valid(spawner): if not is_instance_valid(spawner):
print("RoomTrigger: WARNING - Invalid spawner found, skipping") LogManager.log("RoomTrigger: WARNING - Invalid spawner found, skipping", LogManager.CATEGORY_DUNGEON)
continue continue
if not spawner.has_method("spawn_enemy"): if not spawner.has_method("spawn_enemy"):
print("RoomTrigger: WARNING - Spawner ", spawner.name, " doesn't have spawn_enemy method!") LogManager.log("RoomTrigger: WARNING - Spawner " + str(spawner.name) + " doesn't have spawn_enemy method!", LogManager.CATEGORY_DUNGEON)
continue continue
# CRITICAL: Verify spawner has enemy scenes set # CRITICAL: Verify spawner has enemy scenes set
if "enemy_scenes" in spawner: if "enemy_scenes" in spawner:
if spawner.enemy_scenes.size() == 0: if spawner.enemy_scenes.size() == 0:
print("RoomTrigger: ERROR - Spawner ", spawner.name, " has empty enemy_scenes array! Cannot spawn!") LogManager.log_error("RoomTrigger: ERROR - Spawner " + str(spawner.name) + " has empty enemy_scenes array! Cannot spawn!", LogManager.CATEGORY_DUNGEON)
continue continue
else: else:
print("RoomTrigger: Spawner ", spawner.name, " has ", spawner.enemy_scenes.size(), " enemy scenes available") LogManager.log("RoomTrigger: Spawner " + str(spawner.name) + " has " + str(spawner.enemy_scenes.size()) + " enemy scenes available", LogManager.CATEGORY_DUNGEON)
else: else:
print("RoomTrigger: ERROR - Spawner ", spawner.name, " has no enemy_scenes property!") LogManager.log_error("RoomTrigger: ERROR - Spawner " + str(spawner.name) + " has no enemy_scenes property!", LogManager.CATEGORY_DUNGEON)
continue continue
# CRITICAL: Verify spawner is on server (authority) - only server can spawn # CRITICAL: Verify spawner is on server (authority) - only server can spawn
if multiplayer.has_multiplayer_peer() and not spawner.is_multiplayer_authority(): if multiplayer.has_multiplayer_peer() and not spawner.is_multiplayer_authority():
print("RoomTrigger: WARNING - Spawner ", spawner.name, " is not multiplayer authority! Cannot spawn on client!") LogManager.log("RoomTrigger: WARNING - Spawner " + str(spawner.name) + " is not multiplayer authority! Cannot spawn on client!", LogManager.CATEGORY_DUNGEON)
continue continue
print("RoomTrigger: Calling spawn_enemy() on spawner ", spawner.name, " at ", spawner.global_position) LogManager.log("RoomTrigger: Calling spawn_enemy() on spawner " + str(spawner.name) + " at " + str(spawner.global_position), LogManager.CATEGORY_DUNGEON)
# Spawn enemies from this spawner (spawner will handle max_enemies check) # Spawn enemies from this spawner (spawner will handle max_enemies check)
# NOTE: spawn_enemy() is async (uses await), so we don't await it here - it will execute asynchronously # NOTE: spawn_enemy() is async (uses await), so we don't await it here - it will execute asynchronously
spawner.spawn_enemy() spawner.spawn_enemy()
enemies_spawned = true enemies_spawned = true
print("RoomTrigger: Called spawn_enemy() on ", enemy_spawners.size(), " spawners in room at ", room.x, ", ", room.y) LogManager.log("RoomTrigger: Called spawn_enemy() on " + str(enemy_spawners.size()) + " spawners in room at " + str(room.x) + ", " + str(room.y), LogManager.CATEGORY_DUNGEON)
# Update debug label # Update debug label
_update_debug_label() _update_debug_label()
@@ -315,12 +325,15 @@ func _spawn_room_enemies():
_find_room_entities() # Refresh enemy list after spawning completes _find_room_entities() # Refresh enemy list after spawning completes
_update_debug_label() # Update again after enemies spawn _update_debug_label() # Update again after enemies spawn
else: else:
print("RoomTrigger: No spawners found for room (", room.x, ", ", room.y, ")") LogManager.log("RoomTrigger: No spawners found for room (" + str(room.x) + ", " + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
_update_debug_label() _update_debug_label()
func _cleanup_smoke_puffs(): func _cleanup_smoke_puffs():
# Remove all existing smoke puffs in the scene before spawning new ones # Remove all existing smoke puffs in the scene before spawning new ones
var entities_node = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
var entities_node = null
if game_world:
entities_node = game_world.get_node_or_null("Entities")
if not entities_node: if not entities_node:
entities_node = get_node_or_null("/root/GameWorld/Entities") entities_node = get_node_or_null("/root/GameWorld/Entities")
@@ -331,12 +344,12 @@ func _cleanup_smoke_puffs():
var smoke_puffs_removed = 0 var smoke_puffs_removed = 0
for child in entities_node.get_children(): for child in entities_node.get_children():
if child.is_in_group("smoke_puff") or child.name.begins_with("SmokePuff"): if child.is_in_group("smoke_puff") or child.name.begins_with("SmokePuff"):
print("RoomTrigger: Removing existing smoke puff: ", child.name) LogManager.log("RoomTrigger: Removing existing smoke puff: " + str(child.name), LogManager.CATEGORY_DUNGEON)
child.queue_free() child.queue_free()
smoke_puffs_removed += 1 smoke_puffs_removed += 1
if smoke_puffs_removed > 0: if smoke_puffs_removed > 0:
print("RoomTrigger: Cleaned up ", smoke_puffs_removed, " existing smoke puffs before spawning") LogManager.log("RoomTrigger: Cleaned up " + str(smoke_puffs_removed) + " existing smoke puffs before spawning", LogManager.CATEGORY_DUNGEON)
func _find_room_spawners(): func _find_room_spawners():
# CRITICAL: Clear the list first to avoid accumulating old spawners # CRITICAL: Clear the list first to avoid accumulating old spawners
@@ -345,26 +358,26 @@ func _find_room_spawners():
# Find enemy spawners in this room # Find enemy spawners in this room
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world: if not game_world:
print("RoomTrigger: ERROR - No game_world found when searching for spawners!") LogManager.log_error("RoomTrigger: ERROR - No game_world found when searching for spawners!", LogManager.CATEGORY_DUNGEON)
return return
var entities_node = game_world.get_node_or_null("Entities") var entities_node = game_world.get_node_or_null("Entities")
if not entities_node: if not entities_node:
print("RoomTrigger: ERROR - No Entities node found when searching for spawners!") LogManager.log_error("RoomTrigger: ERROR - No Entities node found when searching for spawners!", LogManager.CATEGORY_DUNGEON)
return return
print("RoomTrigger: ===== Searching for spawners in room (", room.x, ", ", room.y, ", ", room.w, "x", room.h, ") =====") LogManager.log("RoomTrigger: ===== Searching for spawners in room (" + str(room.x) + ", " + str(room.y) + ", " + str(room.w) + "x" + str(room.h) + ") =====", LogManager.CATEGORY_DUNGEON)
print("RoomTrigger: Entities node has ", entities_node.get_child_count(), " children") LogManager.log("RoomTrigger: Entities node has " + str(entities_node.get_child_count()) + " children", LogManager.CATEGORY_DUNGEON)
# Search for spawners (they might be direct children of Entities or in a Spawners node) # Search for spawners (they might be direct children of Entities or in a Spawners node)
var found_spawners_count = 0 var found_spawners_count = 0
for child in entities_node.get_children(): for child in entities_node.get_children():
if child.name.begins_with("EnemySpawner_") or child.is_in_group("enemy_spawner"): if child.name.begins_with("EnemySpawner_") or child.is_in_group("enemy_spawner"):
found_spawners_count += 1 found_spawners_count += 1
print("RoomTrigger: Checking spawner: ", child.name, " at ", child.global_position) LogManager.log("RoomTrigger: Checking spawner: " + str(child.name) + " at " + str(child.global_position), LogManager.CATEGORY_DUNGEON)
if not is_instance_valid(child): if not is_instance_valid(child):
print("RoomTrigger: Spawner is invalid, skipping") LogManager.log("RoomTrigger: Spawner is invalid, skipping", LogManager.CATEGORY_DUNGEON)
continue continue
var spawner_in_room = false var spawner_in_room = false
@@ -372,28 +385,32 @@ func _find_room_spawners():
# First check if spawner has room metadata matching this room # First check if spawner has room metadata matching this room
if child.has_meta("room"): if child.has_meta("room"):
var spawner_room = child.get_meta("room") var spawner_room = child.get_meta("room")
print("RoomTrigger: Spawner has room metadata: (", spawner_room.x if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.y if spawner_room and not spawner_room.is_empty() else "none", ")") var spawner_room_x_str = str(spawner_room.x) if spawner_room and not spawner_room.is_empty() else "none"
var spawner_room_y_str = str(spawner_room.y) if spawner_room and not spawner_room.is_empty() else "none"
LogManager.log("RoomTrigger: Spawner has room metadata: (" + spawner_room_x_str + ", " + spawner_room_y_str + ")", LogManager.CATEGORY_DUNGEON)
if spawner_room and not spawner_room.is_empty(): if spawner_room and not spawner_room.is_empty():
# Compare rooms by position and size # Compare rooms by position and size
if spawner_room.x == room.x and spawner_room.y == room.y and \ if spawner_room.x == room.x and spawner_room.y == room.y and \
spawner_room.w == room.w and spawner_room.h == room.h: spawner_room.w == room.w and spawner_room.h == room.h:
spawner_in_room = true spawner_in_room = true
print("RoomTrigger: ✓ Spawner room matches this trigger room!") LogManager.log("RoomTrigger: ✓ Spawner room matches this trigger room!", LogManager.CATEGORY_DUNGEON)
else: else:
print("RoomTrigger: ✗ Spawner room doesn't match - spawner: (", spawner_room.x, ",", spawner_room.y, ",", spawner_room.w, "x", spawner_room.h, "), trigger: (", room.x, ",", room.y, ",", room.w, "x", room.h, ")") LogManager.log("RoomTrigger: ✗ Spawner room doesn't match - spawner: (" + str(spawner_room.x) + "," + str(spawner_room.y) + "," + str(spawner_room.w) + "x" + str(spawner_room.h) + "), trigger: (" + str(room.x) + "," + str(room.y) + "," + str(room.w) + "x" + str(room.h) + ")", LogManager.CATEGORY_DUNGEON)
# Also check blocking_room metadata (fallback) # Also check blocking_room metadata (fallback)
if not spawner_in_room and child.has_meta("blocking_room"): if not spawner_in_room and child.has_meta("blocking_room"):
var blocking_room = child.get_meta("blocking_room") var blocking_room = child.get_meta("blocking_room")
print("RoomTrigger: Spawner has blocking_room metadata: (", blocking_room.x if blocking_room and not blocking_room.is_empty() else "none", ", ", blocking_room.y if blocking_room and not blocking_room.is_empty() else "none", ")") var blocking_room_x_str = str(blocking_room.x) if blocking_room and not blocking_room.is_empty() else "none"
var blocking_room_y_str = str(blocking_room.y) if blocking_room and not blocking_room.is_empty() else "none"
LogManager.log("RoomTrigger: Spawner has blocking_room metadata: (" + blocking_room_x_str + ", " + blocking_room_y_str + ")", LogManager.CATEGORY_DUNGEON)
if blocking_room and not blocking_room.is_empty(): if blocking_room and not blocking_room.is_empty():
# Compare rooms by position and size # Compare rooms by position and size
if blocking_room.x == room.x and blocking_room.y == room.y and \ if blocking_room.x == room.x and blocking_room.y == room.y and \
blocking_room.w == room.w and blocking_room.h == room.h: blocking_room.w == room.w and blocking_room.h == room.h:
spawner_in_room = true spawner_in_room = true
print("RoomTrigger: ✓ Spawner blocking_room matches this trigger room!") LogManager.log("RoomTrigger: ✓ Spawner blocking_room matches this trigger room!", LogManager.CATEGORY_DUNGEON)
else: else:
print("RoomTrigger: ✗ Spawner blocking_room doesn't match - spawner: (", blocking_room.x, ",", blocking_room.y, ",", blocking_room.w, "x", blocking_room.h, "), trigger: (", room.x, ",", room.y, ",", room.w, "x", room.h, ")") LogManager.log("RoomTrigger: ✗ Spawner blocking_room doesn't match - spawner: (" + str(blocking_room.x) + "," + str(blocking_room.y) + "," + str(blocking_room.w) + "x" + str(blocking_room.h) + "), trigger: (" + str(room.x) + "," + str(room.y) + "," + str(room.w) + "x" + str(room.h) + ")", LogManager.CATEGORY_DUNGEON)
# Also check by position (fallback if no room metadata) # Also check by position (fallback if no room metadata)
if not spawner_in_room: if not spawner_in_room:
@@ -405,22 +422,22 @@ func _find_room_spawners():
var room_min_y = room.y + 2 var room_min_y = room.y + 2
var room_max_y = room.y + room.h - 2 var room_max_y = room.y + room.h - 2
print("RoomTrigger: Checking by position: spawner at tile (", spawner_tile_x, ", ", spawner_tile_y, "), room bounds: (", room_min_x, "-", room_max_x, ", ", room_min_y, "-", room_max_y, ")") LogManager.log("RoomTrigger: Checking by position: spawner at tile (" + str(spawner_tile_x) + ", " + str(spawner_tile_y) + "), room bounds: (" + str(room_min_x) + "-" + str(room_max_x) + ", " + str(room_min_y) + "-" + str(room_max_y) + ")", LogManager.CATEGORY_DUNGEON)
if spawner_tile_x >= room_min_x and spawner_tile_x < room_max_x and \ if spawner_tile_x >= room_min_x and spawner_tile_x < room_max_x and \
spawner_tile_y >= room_min_y and spawner_tile_y < room_max_y: spawner_tile_y >= room_min_y and spawner_tile_y < room_max_y:
spawner_in_room = true spawner_in_room = true
print("RoomTrigger: Spawner is within room bounds!") LogManager.log("RoomTrigger: Spawner is within room bounds!", LogManager.CATEGORY_DUNGEON)
if spawner_in_room and not child in enemy_spawners: if spawner_in_room and not child in enemy_spawners:
enemy_spawners.append(child) enemy_spawners.append(child)
print("RoomTrigger: ✓ Added spawner ", child.name, " to this room trigger") LogManager.log("RoomTrigger: ✓ Added spawner " + str(child.name) + " to this room trigger", LogManager.CATEGORY_DUNGEON)
elif spawner_in_room: elif spawner_in_room:
print("RoomTrigger: Spawner already in list, skipping") LogManager.log("RoomTrigger: Spawner already in list, skipping", LogManager.CATEGORY_DUNGEON)
else: else:
print("RoomTrigger: Spawner is NOT in this room, skipping") LogManager.log("RoomTrigger: Spawner is NOT in this room, skipping", LogManager.CATEGORY_DUNGEON)
print("RoomTrigger: Total spawners found: ", found_spawners_count, ", spawners in this room: ", enemy_spawners.size()) LogManager.log("RoomTrigger: Total spawners found: " + str(found_spawners_count) + ", spawners in this room: " + str(enemy_spawners.size()), LogManager.CATEGORY_DUNGEON)
# Update debug label after finding entities # Update debug label after finding entities
call_deferred("_update_debug_label") call_deferred("_update_debug_label")
@@ -430,13 +447,13 @@ func _create_debug_label():
# Only create if debug mode is enabled # Only create if debug mode is enabled
var network_manager = get_node_or_null("/root/NetworkManager") var network_manager = get_node_or_null("/root/NetworkManager")
if not network_manager: if not network_manager:
print("RoomTrigger: NetworkManager not found, skipping debug label") LogManager.log("RoomTrigger: NetworkManager not found, skipping debug label", LogManager.CATEGORY_DUNGEON)
return return
if not network_manager.show_room_labels: if not network_manager.show_room_labels:
print("RoomTrigger: Debug mode not enabled (show_room_labels = false), skipping debug label for room (", room.x, ", ", room.y, ")") LogManager.log("RoomTrigger: Debug mode not enabled (show_room_labels = false), skipping debug label for room (" + str(room.x) + ", " + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
return return
print("RoomTrigger: Creating debug label for room (", room.x, ", ", room.y, ") - debug mode enabled") LogManager.log("RoomTrigger: Creating debug label for room (" + str(room.x) + ", " + str(room.y) + ") - debug mode enabled", LogManager.CATEGORY_DUNGEON)
# Skip if label already exists # Skip if label already exists
if debug_label and is_instance_valid(debug_label): if debug_label and is_instance_valid(debug_label):

View File

@@ -123,17 +123,30 @@ func _on_body_entered(body):
# Hit successful - play impact sound and deal damage # Hit successful - play impact sound and deal damage
$SfxImpact.play() $SfxImpact.play()
# Use game_world to route damage request instead of direct RPC to avoid node path issues
var game_world = get_tree().get_first_node_in_group("game_world")
var enemy_name = body.name
var enemy_index = body.get_meta("enemy_index") if body.has_meta("enemy_index") else -1
if game_world and game_world.has_method("_request_enemy_damage"):
if multiplayer.is_server():
# Server can call directly
game_world._request_enemy_damage(enemy_name, enemy_index, damage, attacker_pos, is_crit)
else:
# Client sends RPC to server
game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, is_crit)
else:
# Fallback: try direct call (may fail if node path doesn't match)
var enemy_peer_id = body.get_multiplayer_authority() var enemy_peer_id = body.get_multiplayer_authority()
if enemy_peer_id != 0: if enemy_peer_id != 0:
# If enemy is on the same peer (server), call directly
if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id(): if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id():
body.rpc_take_damage(damage, attacker_pos, is_crit) body.rpc_take_damage(damage, attacker_pos, is_crit)
else: else:
# Send RPC to enemy's authority (server) - clients can do this!
body.rpc_take_damage.rpc_id(enemy_peer_id, damage, attacker_pos, is_crit) body.rpc_take_damage.rpc_id(enemy_peer_id, damage, attacker_pos, is_crit)
else: else:
# Fallback: broadcast if we can't get peer_id
body.rpc_take_damage.rpc(damage, attacker_pos, is_crit) body.rpc_take_damage.rpc(damage, attacker_pos, is_crit)
# Debug print - handle null player_owner safely # Debug print - handle null player_owner safely
var owner_name: String = "none" var owner_name: String = "none"
var is_authority: bool = false var is_authority: bool = false
@@ -149,16 +162,46 @@ func _on_body_entered(body):
# Boxes have health property # Boxes have health property
body.health -= damage body.health -= damage
if body.health <= 0: if body.health <= 0:
# Break locally first # Get object identifier - prefer consistent name (InteractableObject_X) or use object_index meta
var obj_name = body.name
var obj_index = -1
# Check if object has object_index meta (set when spawned in dungeon)
if body.has_meta("object_index"):
obj_index = body.get_meta("object_index")
# Use consistent naming if we have the index
if obj_index >= 0:
obj_name = "InteractableObject_%d" % obj_index
# Fallback: if name doesn't start with "InteractableObject_", try to find consistent name
if not obj_name.begins_with("InteractableObject_") and obj_index < 0:
# Try to find object by position or other means
# For now, just use the name as-is and log a warning
print("Sword projectile: Warning - object ", body.name, " doesn't have consistent naming!")
# Sync break to server (which will broadcast to all clients)
# Use game_world to route the RPC to avoid node path resolution issues
if multiplayer.has_multiplayer_peer():
var game_world = get_tree().get_first_node_in_group("game_world")
# Only send RPC if game_world exists and is valid
if game_world and is_instance_valid(game_world) and game_world.is_inside_tree() and game_world.has_method("_sync_object_break"):
if multiplayer.is_server():
# Server: broadcast to ready clients (breaks locally too via _sync_break)
if game_world.has_method("_rpc_to_ready_peers"):
game_world._rpc_to_ready_peers("_sync_object_break", [obj_name])
print("Sword projectile synced box break to all clients: ", obj_name)
else:
# Client: send request to server (server will break locally and broadcast to all)
# Also send object_index if available for fallback lookup
game_world._sync_object_break.rpc_id(1, obj_name)
print("Sword projectile requested box break on server: ", obj_name, " (index: ", obj_index, ")")
else:
print("Sword projectile: GameWorld not ready, skipping box break sync for ", obj_name)
# Break locally AFTER syncing (so node still exists for RPC path resolution)
if body.has_method("_break_into_pieces"): if body.has_method("_break_into_pieces"):
body._break_into_pieces() body._break_into_pieces()
print("Sword projectile broke box locally: ", body.name) print("Sword projectile broke box locally: ", body.name)
# Sync break to OTHER clients via RPC
if multiplayer.has_multiplayer_peer():
if body.has_method("_sync_break"):
body._sync_break.rpc()
print("Sword projectile synced box break to other clients")
print("Sword projectile hit object: ", body.name) print("Sword projectile hit object: ", body.name)
# Push the hit target away slightly (only for non-enemies) # Push the hit target away slightly (only for non-enemies)

View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

373
src/webrtc/LICENSE.libjuice Normal file
View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -0,0 +1,35 @@
/*
*
* Copyright (c) 2001-2017 Cisco Systems, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* Neither the name of the Cisco Systems, Inc. nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/

553
src/webrtc/LICENSE.mbedtls Normal file
View File

@@ -0,0 +1,553 @@
Mbed TLS files are provided under a dual [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)
OR [GPL-2.0-or-later](https://spdx.org/licenses/GPL-2.0-or-later.html) license.
This means that users may choose which of these licenses they take the code
under.
The full text of each of these licenses is given below.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===============================================================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

21
src/webrtc/LICENSE.plog Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Sergey Podobry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,27 @@
Copyright (c) 2015, Randall Stewart and Michael Tuexen
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of usrsctp nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Godot Engine
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>libwebrtc_native.macos.template_debug.universal.dylib</string>
<key>CFBundleIdentifier</key>
<string>org.godotengine.webrtc-native</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDisplayName</key>
<string>libwebrtc_native.macos.template_debug.universal</string>
<key>CFBundleName</key>
<string>webrtc_native</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>libwebrtc_native.macos.template_release.universal.dylib</string>
<key>CFBundleIdentifier</key>
<string>org.godotengine.webrtc-native</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDisplayName</key>
<string>libwebrtc_native.macos.template_release.universal</string>
<key>CFBundleName</key>
<string>webrtc_native</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,30 @@
[configuration]
entry_symbol = "webrtc_extension_init"
compatibility_minimum = 4.1
[libraries]
linux.debug.x86_64 = "lib/libwebrtc_native.linux.template_debug.x86_64.so"
linux.debug.x86_32 = "lib/libwebrtc_native.linux.template_debug.x86_32.so"
linux.debug.arm64 = "lib/libwebrtc_native.linux.template_debug.arm64.so"
linux.debug.arm32 = "lib/libwebrtc_native.linux.template_debug.arm32.so"
macos.debug = "lib/libwebrtc_native.macos.template_debug.universal.framework"
windows.debug.x86_64 = "lib/libwebrtc_native.windows.template_debug.x86_64.dll"
windows.debug.x86_32 = "lib/libwebrtc_native.windows.template_debug.x86_32.dll"
android.debug.arm64 = "lib/libwebrtc_native.android.template_debug.arm64.so"
android.debug.x86_64 = "lib/libwebrtc_native.android.template_debug.x86_64.so"
ios.debug.arm64 = "lib/libwebrtc_native.ios.template_debug.arm64.dylib"
ios.debug.x86_64 = "lib/libwebrtc_native.ios.template_debug.x86_64.simulator.dylib"
linux.release.x86_64 = "lib/libwebrtc_native.linux.template_release.x86_64.so"
linux.release.x86_32 = "lib/libwebrtc_native.linux.template_release.x86_32.so"
linux.release.arm64 = "lib/libwebrtc_native.linux.template_release.arm64.so"
linux.release.arm32 = "lib/libwebrtc_native.linux.template_release.arm32.so"
macos.release = "lib/libwebrtc_native.macos.template_release.universal.framework"
windows.release.x86_64 = "lib/libwebrtc_native.windows.template_release.x86_64.dll"
windows.release.x86_32 = "lib/libwebrtc_native.windows.template_release.x86_32.dll"
android.release.arm64 = "lib/libwebrtc_native.android.template_release.arm64.so"
android.release.x86_64 = "lib/libwebrtc_native.android.template_release.x86_64.so"
ios.release.arm64 = "lib/libwebrtc_native.ios.template_release.arm64.dylib"
ios.release.x86_64 = "lib/libwebrtc_native.ios.template_release.x86_64.simulator.dylib"

View File

@@ -0,0 +1 @@
uid://o5h08t2r11hh