Compare commits
19 Commits
f62eeab50d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a4792759e6 | |||
| e2fa2ba8a9 | |||
| aa27725c4e | |||
| cf7d73ba08 | |||
| 83544fe3d2 | |||
| 391c000a73 | |||
| 44b758360c | |||
| 385b4f690e | |||
| b3651f8027 | |||
| 7327f073d8 | |||
| b993c6b3e6 | |||
| e0da6c4f00 | |||
| 597bfe1723 | |||
| 01d6128209 | |||
| 5c98f1638a | |||
| 526d30c569 | |||
| c835f932b0 | |||
| 4743c307a6 | |||
| 456d876847 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -22,6 +22,11 @@ frontend/dist/
|
|||||||
# WebGL build artifacts (generated by Unity)
|
# WebGL build artifacts (generated by Unity)
|
||||||
build/
|
build/
|
||||||
nouveau_build/
|
nouveau_build/
|
||||||
|
build_mai/
|
||||||
|
|
||||||
|
# Exception: frontend unity-build static assets (committed for deployment)
|
||||||
|
!frontend/public/unity-build/
|
||||||
|
!frontend/public/unity-build/**
|
||||||
|
|
||||||
# Dev-only tools (buildgate was used for manual dev deploys, remplacé par Coolify)
|
# Dev-only tools (buildgate was used for manual dev deploys, remplacé par Coolify)
|
||||||
rolld_buildgate/
|
rolld_buildgate/
|
||||||
|
|||||||
159
README.md
Normal file
159
README.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<h1>ROLL'D</h1>
|
||||||
|
|
||||||
|
<p><strong>Browser-based marble MMO — multiplayer physics, real-time leaderboards, playable directly in your browser.</strong></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/Unity-6000.0-black?style=for-the-badge&logo=unity&logoColor=white" alt="Unity 6" />
|
||||||
|
<img src="https://img.shields.io/badge/WebGL-build-E34F26?style=for-the-badge&logo=webgl&logoColor=white" alt="WebGL" />
|
||||||
|
<img src="https://img.shields.io/badge/Colyseus-0.17-6C47FF?style=for-the-badge&logo=node.js&logoColor=white" alt="Colyseus" />
|
||||||
|
<img src="https://img.shields.io/badge/React-19-61DAFB?style=for-the-badge&logo=react&logoColor=black" alt="React" />
|
||||||
|
<img src="https://img.shields.io/badge/Vite-5-646CFF?style=for-the-badge&logo=vite&logoColor=white" alt="Vite" />
|
||||||
|
<img src="https://img.shields.io/badge/Tailwind_CSS-3-38BDF8?style=for-the-badge&logo=tailwindcss&logoColor=white" alt="Tailwind" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://rolld.kerboul.me"><img src="https://img.shields.io/badge/Play_Now-rolld.kerboul.me-22c55e?style=for-the-badge&logo=googlechrome&logoColor=white" alt="Play Now" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is ROLL'D?
|
||||||
|
|
||||||
|
ROLL'D is a multiplayer marble game that runs entirely in the browser via Unity WebGL. Players control a physics-based ball in a shared 3D arena, competing for distance, speed, and style. The game features real-time synchronisation at 60 Hz, in-game chat, and persistent leaderboards.
|
||||||
|
|
||||||
|
No install. No account. Just open the page and roll.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Real-time multiplayer** - up to 20 players per room, 60 Hz state sync via Colyseus WebSockets
|
||||||
|
- **Physics-based gameplay** - Unity Rigidbody, jump charge, gel pads (speed boosts), ball-to-ball bumps
|
||||||
|
- **Room lobby** - browse open rooms, create your own, choose your colour and name
|
||||||
|
- **In-game chat** - accessible in-game (T key) and on the dedicated website chat page
|
||||||
|
- **Live leaderboards** - distance, max speed, jumps, bumps, playtime, updated every 30 seconds
|
||||||
|
- **Spectator camera** - orbiting camera while in lobby or after disconnecting
|
||||||
|
- **WebGL-native** - no plugins, no downloads, runs in Chrome/Firefox/Edge
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
rolld/
|
||||||
|
├── game/ # Unity 6 project (WebGL build)
|
||||||
|
│ └── Assets/Scripts/
|
||||||
|
│ ├── Network/ # Colyseus SDK integration, schema, lobby UI
|
||||||
|
│ ├── Stats/ # StatsTracker - periodic HTTP upload
|
||||||
|
│ └── UI/ # IMGUI in-game HUD, chat, keybinds
|
||||||
|
│
|
||||||
|
├── rolld_backend/game/ # Colyseus 0.17 game server (Node.js)
|
||||||
|
│ └── src/
|
||||||
|
│ ├── rooms/ # ArenaRoom - game state machine
|
||||||
|
│ ├── schema/ # Colyseus schema (Player + GameState)
|
||||||
|
│ ├── stats/ # StatsManager - JSON persistence
|
||||||
|
│ └── chat/ # ChatManager - in-memory history
|
||||||
|
│
|
||||||
|
└── frontend/ # React + Vite + Tailwind SPA
|
||||||
|
└── src/
|
||||||
|
├── pages/ # Home, Stats leaderboard, Chat
|
||||||
|
└── components/ # NavBar, GameCanvas (Unity embed)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser
|
||||||
|
└── Unity WebGL (GameCanvas iframe)
|
||||||
|
└── Colyseus SDK (WebSocket wss://)
|
||||||
|
└── ArenaRoom (Node.js)
|
||||||
|
└── Broadcast state @60 Hz
|
||||||
|
|
||||||
|
Browser
|
||||||
|
└── React SPA
|
||||||
|
└── REST API (HTTPS)
|
||||||
|
├── GET /stats/leaderboard/:key
|
||||||
|
├── GET /chat/history?since=
|
||||||
|
└── POST /stats/update (from Unity every 30s)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|---|---|
|
||||||
|
| Game engine | Unity 6 LTS, C# |
|
||||||
|
| Multiplayer | Colyseus 0.17 (Node.js + WebSocket) |
|
||||||
|
| Frontend | React 19, Vite 5, Tailwind CSS 3 |
|
||||||
|
| Deployment | Docker, Coolify, nginx |
|
||||||
|
| Self-hosted | Proxmox LXC, Gitea, Traefik reverse proxy |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running locally
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 20+
|
||||||
|
- Unity 6000.x (for game builds only)
|
||||||
|
|
||||||
|
### Game server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd rolld_backend/game
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Server starts on `ws://localhost:2567`.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:5173`. The frontend points to the production game server by default - edit `src/pages/StatsPage.jsx` and `src/components/GameCanvas.jsx` to switch to localhost.
|
||||||
|
|
||||||
|
### Unity (optional)
|
||||||
|
|
||||||
|
Open `game/` in Unity 6. The server URL is hardcoded in `Assets/Scripts/Network/NetworkManager.cs`. Switch to `wss://game.rolld.kerboul.me` for prod or `ws://localhost:2567` for local testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| WASD / Arrow keys | Move |
|
||||||
|
| Space (hold) | Charge jump |
|
||||||
|
| Space (release) | Jump |
|
||||||
|
| T | Open chat |
|
||||||
|
| Escape | Close chat |
|
||||||
|
| Tab | Show keybindings |
|
||||||
|
| Backtick (`) | Debug network info |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Live deployment
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/Frontend-rolld.kerboul.me-22c55e?style=flat-square&logo=nginx&logoColor=white" alt="Frontend" />
|
||||||
|
<img src="https://img.shields.io/badge/Game_server-game.rolld.kerboul.me-6C47FF?style=flat-square&logo=node.js&logoColor=white" alt="Game server" />
|
||||||
|
<img src="https://img.shields.io/badge/Self_hosted-Proxmox_homelab-E57000?style=flat-square&logo=proxmox&logoColor=white" alt="Self-hosted" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
The stack runs on a self-hosted Proxmox homelab cluster. Coolify handles container orchestration and auto-deployment on git push. Traefik manages HTTPS termination.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
MIT - do whatever you want with it.
|
||||||
BIN
frontend/public/unity-build/Build/last_build.data
Normal file
BIN
frontend/public/unity-build/Build/last_build.data
Normal file
Binary file not shown.
57
frontend/public/unity-build/Build/last_build.framework.js
Normal file
57
frontend/public/unity-build/Build/last_build.framework.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/public/unity-build/Build/last_build.loader.js
Normal file
1
frontend/public/unity-build/Build/last_build.loader.js
Normal file
File diff suppressed because one or more lines are too long
BIN
frontend/public/unity-build/Build/last_build.wasm
Normal file
BIN
frontend/public/unity-build/Build/last_build.wasm
Normal file
Binary file not shown.
@@ -1,28 +1,41 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { IS_DEV } from './env'
|
import { IS_DEV } from './env'
|
||||||
import DevBanner from './components/DevBanner'
|
import DevBanner from './components/DevBanner'
|
||||||
|
import NavBar from './components/NavBar'
|
||||||
import Hero from './components/Hero'
|
import Hero from './components/Hero'
|
||||||
import GelShowcase from './components/GelShowcase'
|
import GelShowcase from './components/GelShowcase'
|
||||||
import KerboulistanBanner from './components/KerboulistanBanner'
|
import KerboulistanBanner from './components/KerboulistanBanner'
|
||||||
import GameCanvas from './components/GameCanvas'
|
import GameCanvas from './components/GameCanvas'
|
||||||
import Footer from './components/Footer'
|
import Footer from './components/Footer'
|
||||||
|
import StatsPage from './pages/StatsPage'
|
||||||
|
import ChatPage from './pages/ChatPage'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isPlaying, setIsPlaying] = useState(false)
|
const [page, setPage] = useState('home')
|
||||||
|
|
||||||
if (isPlaying) {
|
if (page === 'play') {
|
||||||
return <GameCanvas onBack={() => setIsPlaying(false)} />
|
return <GameCanvas onBack={() => setPage('home')} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<DevBanner />
|
<DevBanner />
|
||||||
{/* Offset content when dev banner is visible */}
|
<NavBar page={page} setPage={setPage} />
|
||||||
{IS_DEV && <div className="h-8" />}
|
|
||||||
<Hero onPlay={() => setIsPlaying(true)} />
|
{page === 'home' && (
|
||||||
<GelShowcase />
|
<>
|
||||||
<KerboulistanBanner />
|
{IS_DEV && <div className="h-8" />}
|
||||||
<Footer />
|
<div className="pt-14">
|
||||||
|
<Hero onPlay={() => setPage('play')} />
|
||||||
|
<GelShowcase />
|
||||||
|
<KerboulistanBanner />
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{page === 'stats' && <StatsPage />}
|
||||||
|
{page === 'chat' && <ChatPage />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import { useState, useEffect, useCallback } from 'react'
|
|||||||
// Check if Unity build files exist
|
// Check if Unity build files exist
|
||||||
const UNITY_BUILD_PATH = '/unity-build/Build'
|
const UNITY_BUILD_PATH = '/unity-build/Build'
|
||||||
// Cache-busting version — update this after each Unity build
|
// Cache-busting version — update this after each Unity build
|
||||||
const UNITY_BUILD_VERSION = '20260310c'
|
const UNITY_BUILD_VERSION = '20260518'
|
||||||
const LOADER_URL = `${UNITY_BUILD_PATH}/nouveau_build.loader.js?v=${UNITY_BUILD_VERSION}`
|
const BUILD_PREFIX = 'last_build'
|
||||||
|
const LOADER_URL = `${UNITY_BUILD_PATH}/${BUILD_PREFIX}.loader.js?v=${UNITY_BUILD_VERSION}`
|
||||||
|
|
||||||
// Game server URL (Colyseus WebSocket)
|
|
||||||
const GAME_SERVER_URL = import.meta.env.VITE_GAME_SERVER_URL || 'ws://localhost:2567'
|
|
||||||
|
|
||||||
export default function GameCanvas({ onBack }) {
|
export default function GameCanvas({ onBack }) {
|
||||||
const [loadingProgress, setLoadingProgress] = useState(0)
|
const [loadingProgress, setLoadingProgress] = useState(0)
|
||||||
@@ -39,9 +38,9 @@ export default function GameCanvas({ onBack }) {
|
|||||||
if (typeof window.createUnityInstance === 'function') {
|
if (typeof window.createUnityInstance === 'function') {
|
||||||
const canvas = document.getElementById('unity-canvas')
|
const canvas = document.getElementById('unity-canvas')
|
||||||
window.createUnityInstance(canvas, {
|
window.createUnityInstance(canvas, {
|
||||||
dataUrl: `${UNITY_BUILD_PATH}/nouveau_build.data?v=${UNITY_BUILD_VERSION}`,
|
dataUrl: `${UNITY_BUILD_PATH}/${BUILD_PREFIX}.data?v=${UNITY_BUILD_VERSION}`,
|
||||||
frameworkUrl: `${UNITY_BUILD_PATH}/nouveau_build.framework.js?v=${UNITY_BUILD_VERSION}`,
|
frameworkUrl: `${UNITY_BUILD_PATH}/${BUILD_PREFIX}.framework.js?v=${UNITY_BUILD_VERSION}`,
|
||||||
codeUrl: `${UNITY_BUILD_PATH}/nouveau_build.wasm?v=${UNITY_BUILD_VERSION}`,
|
codeUrl: `${UNITY_BUILD_PATH}/${BUILD_PREFIX}.wasm?v=${UNITY_BUILD_VERSION}`,
|
||||||
streamingAssetsUrl: '/unity-build/StreamingAssets',
|
streamingAssetsUrl: '/unity-build/StreamingAssets',
|
||||||
companyName: 'ROLLD',
|
companyName: 'ROLLD',
|
||||||
productName: 'ROLLD',
|
productName: 'ROLLD',
|
||||||
@@ -79,9 +78,7 @@ export default function GameCanvas({ onBack }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass game server URL to Unity's NetworkManager
|
console.log('[ROLLD] Unity loaded')
|
||||||
instance.SendMessage('NetworkManager', 'SetServerURL', GAME_SERVER_URL)
|
|
||||||
console.log('[ROLLD] Unity loaded, server URL sent:', GAME_SERVER_URL)
|
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
})
|
})
|
||||||
@@ -153,10 +150,10 @@ export default function GameCanvas({ onBack }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="glass rounded-xl p-4 text-left text-sm text-rolld-muted font-mono max-w-sm w-full">
|
<div className="glass rounded-xl p-4 text-left text-sm text-rolld-muted font-mono max-w-sm w-full">
|
||||||
<p className="text-rolld-accent-light mb-2">Fichiers requis :</p>
|
<p className="text-rolld-accent-light mb-2">Fichiers requis :</p>
|
||||||
<p>├── nouveau_build.loader.js</p>
|
<p>├── build_mai.loader.js</p>
|
||||||
<p>├── nouveau_build.data</p>
|
<p>├── build_mai.data</p>
|
||||||
<p>├── nouveau_build.framework.js</p>
|
<p>├── build_mai.framework.js</p>
|
||||||
<p>└── nouveau_build.wasm</p>
|
<p>└── build_mai.wasm</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
50
frontend/src/components/NavBar.jsx
Normal file
50
frontend/src/components/NavBar.jsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { theme } from '../env'
|
||||||
|
|
||||||
|
const LINKS = [
|
||||||
|
{ id: 'home', label: 'Accueil' },
|
||||||
|
{ id: 'stats', label: 'Stats' },
|
||||||
|
{ id: 'chat', label: 'Chat' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function NavBar({ page, setPage }) {
|
||||||
|
return (
|
||||||
|
<nav className="fixed top-0 left-0 right-0 z-50 glass border-b border-rolld-border/60">
|
||||||
|
<div className="max-w-6xl mx-auto px-4 h-14 flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
onClick={() => setPage('home')}
|
||||||
|
className="font-black text-lg tracking-tight"
|
||||||
|
style={{ color: theme.accent }}
|
||||||
|
>
|
||||||
|
ROLL'D
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{LINKS.map(link => (
|
||||||
|
<button
|
||||||
|
key={link.id}
|
||||||
|
onClick={() => setPage(link.id)}
|
||||||
|
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 ${
|
||||||
|
page === link.id
|
||||||
|
? 'text-white'
|
||||||
|
: 'text-rolld-muted hover:text-rolld-text'
|
||||||
|
}`}
|
||||||
|
style={page === link.id ? { background: `rgba(${theme.accentRgb}, 0.2)`, color: theme.accentLight } : {}}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setPage('play')}
|
||||||
|
className="ml-3 px-4 py-2 rounded-xl text-sm font-bold text-white transition-all duration-200 hover:scale-105"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `linear-gradient(to right, ${theme.accent}, ${theme.gradientTo})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Jouer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
185
frontend/src/pages/ChatPage.jsx
Normal file
185
frontend/src/pages/ChatPage.jsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
|
import { theme } from '../env'
|
||||||
|
|
||||||
|
const SERVER = 'https://game.rolld.kerboul.me'
|
||||||
|
const POLL_INTERVAL = 3000
|
||||||
|
|
||||||
|
export default function ChatPage() {
|
||||||
|
const [messages, setMessages] = useState([])
|
||||||
|
const [inputText, setInputText] = useState('')
|
||||||
|
const [playerName, setPlayerName] = useState(() => localStorage.getItem('rolld_chat_name') || '')
|
||||||
|
const [editingName, setEditingName] = useState(!localStorage.getItem('rolld_chat_name'))
|
||||||
|
const [nameInput, setNameInput] = useState(playerName)
|
||||||
|
const [sending, setSending] = useState(false)
|
||||||
|
const lastTimestampRef = useRef(0)
|
||||||
|
const bottomRef = useRef(null)
|
||||||
|
const inputRef = useRef(null)
|
||||||
|
|
||||||
|
const fetchMessages = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${SERVER}/chat/history?since=${lastTimestampRef.current}`)
|
||||||
|
if (!res.ok) return
|
||||||
|
const data = await res.json()
|
||||||
|
if (!Array.isArray(data) || data.length === 0) return
|
||||||
|
setMessages(prev => {
|
||||||
|
const updated = [...prev, ...data]
|
||||||
|
// Deduplicate by id
|
||||||
|
const seen = new Set()
|
||||||
|
const deduped = updated.filter(m => {
|
||||||
|
if (seen.has(m.id)) return false
|
||||||
|
seen.add(m.id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return deduped.slice(-200)
|
||||||
|
})
|
||||||
|
const maxTs = Math.max(...data.map(m => m.timestamp))
|
||||||
|
if (maxTs > lastTimestampRef.current) lastTimestampRef.current = maxTs
|
||||||
|
} catch {
|
||||||
|
// silently ignore network errors
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMessages()
|
||||||
|
const id = setInterval(fetchMessages, POLL_INTERVAL)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}, [fetchMessages])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}, [messages])
|
||||||
|
|
||||||
|
const saveName = () => {
|
||||||
|
const n = nameInput.trim()
|
||||||
|
if (!n) return
|
||||||
|
setPlayerName(n)
|
||||||
|
localStorage.setItem('rolld_chat_name', n)
|
||||||
|
setEditingName(false)
|
||||||
|
setTimeout(() => inputRef.current?.focus(), 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMessage = async () => {
|
||||||
|
const text = inputText.trim()
|
||||||
|
if (!text || !playerName || sending) return
|
||||||
|
setSending(true)
|
||||||
|
setInputText('')
|
||||||
|
try {
|
||||||
|
await fetch(`${SERVER}/chat/send`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: playerName, text }),
|
||||||
|
})
|
||||||
|
// Immediately poll to get our own message back
|
||||||
|
await fetchMessages()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setSending(false)
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
sendMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (ts) => {
|
||||||
|
const d = new Date(ts)
|
||||||
|
return d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen pt-20 px-4 flex flex-col">
|
||||||
|
<div className="max-w-2xl mx-auto w-full flex flex-col flex-1" style={{ maxHeight: 'calc(100vh - 5rem)' }}>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-black text-rolld-text">Chat général</h1>
|
||||||
|
<p className="text-rolld-muted text-sm mt-1">Partagé entre le jeu et le site — rafraîchi toutes les 3s</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Name badge */}
|
||||||
|
{!editingName ? (
|
||||||
|
<button
|
||||||
|
onClick={() => { setNameInput(playerName); setEditingName(true) }}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 rounded-xl border border-rolld-border bg-rolld-surface text-sm hover:border-rolld-accent/40 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-rolld-muted">Joueur :</span>
|
||||||
|
<span className="font-bold" style={{ color: theme.accentLight }}>{playerName}</span>
|
||||||
|
<span className="text-rolld-muted text-xs">✏️</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={nameInput}
|
||||||
|
onChange={e => setNameInput(e.target.value)}
|
||||||
|
onKeyDown={e => e.key === 'Enter' && saveName()}
|
||||||
|
placeholder="Ton pseudo"
|
||||||
|
maxLength={24}
|
||||||
|
className="w-36 px-3 py-1.5 rounded-xl border border-rolld-accent/60 bg-rolld-surface text-rolld-text text-sm outline-none focus:border-rolld-accent"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={saveName}
|
||||||
|
className="px-3 py-1.5 rounded-xl text-sm font-bold text-white"
|
||||||
|
style={{ background: theme.accent }}
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages */}
|
||||||
|
<div className="flex-1 overflow-y-auto rounded-2xl border border-rolld-border bg-rolld-surface p-4 space-y-1 min-h-0">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="h-full flex items-center justify-center text-rolld-muted text-sm">
|
||||||
|
Aucun message pour l'instant. Soyez le premier !
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
messages.map(msg => (
|
||||||
|
<div key={msg.id} className="flex items-baseline gap-2 py-0.5 group">
|
||||||
|
<span className="text-rolld-muted text-xs font-mono shrink-0 opacity-60 group-hover:opacity-100 transition-opacity w-11">
|
||||||
|
{formatTime(msg.timestamp)}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-bold shrink-0" style={{ color: theme.accentLight }}>
|
||||||
|
{msg.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-rolld-text break-words min-w-0">
|
||||||
|
{msg.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={bottomRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div className="mt-3 flex gap-2 pb-4">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputText}
|
||||||
|
onChange={e => setInputText(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={playerName ? 'Écrire un message… (Entrée pour envoyer)' : 'Choisissez d\'abord un pseudo →'}
|
||||||
|
maxLength={200}
|
||||||
|
disabled={!playerName || editingName}
|
||||||
|
className="flex-1 px-4 py-3 rounded-xl border border-rolld-border bg-rolld-surface text-rolld-text text-sm outline-none focus:border-rolld-accent/60 disabled:opacity-40 transition-colors"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={sendMessage}
|
||||||
|
disabled={!inputText.trim() || !playerName || sending || editingName}
|
||||||
|
className="px-5 py-3 rounded-xl text-sm font-bold text-white transition-all duration-200 hover:scale-105 disabled:opacity-40 disabled:hover:scale-100"
|
||||||
|
style={{ backgroundImage: `linear-gradient(to right, ${theme.accent}, ${theme.gradientTo})` }}
|
||||||
|
>
|
||||||
|
Envoyer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
141
frontend/src/pages/StatsPage.jsx
Normal file
141
frontend/src/pages/StatsPage.jsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { theme } from '../env'
|
||||||
|
|
||||||
|
const SERVER = 'https://game.rolld.kerboul.me'
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{ key: 'totalDistance', label: 'Distance', unit: 'm', format: v => Math.round(v ?? 0).toLocaleString('fr-FR') },
|
||||||
|
{ key: 'maxSpeed', label: 'Vitesse max', unit: 'm/s', format: v => (v ?? 0).toFixed(1) },
|
||||||
|
{ key: 'totalJumps', label: 'Sauts', unit: '', format: v => (v ?? 0).toLocaleString('fr-FR') },
|
||||||
|
{ key: 'bumpsGiven', label: 'Bumps', unit: '', format: v => (v ?? 0).toLocaleString('fr-FR') },
|
||||||
|
{ key: 'totalPlaytime',label: 'Temps de jeu',unit: '', format: v => {
|
||||||
|
const total = Math.round(v ?? 0)
|
||||||
|
const h = Math.floor(total / 3600)
|
||||||
|
const m = Math.floor((total % 3600) / 60)
|
||||||
|
const s = total % 60
|
||||||
|
return h > 0 ? `${h}h ${m}m` : `${m}m ${s}s`
|
||||||
|
}},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function StatsPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState(TABS[0].key)
|
||||||
|
const [rows, setRows] = useState([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [lastRefresh, setLastRefresh] = useState(null)
|
||||||
|
|
||||||
|
const fetchLeaderboard = useCallback(async (key) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${SERVER}/stats/leaderboard/${key}`)
|
||||||
|
if (!res.ok) throw new Error(res.statusText)
|
||||||
|
const data = await res.json()
|
||||||
|
setRows(data)
|
||||||
|
setLastRefresh(new Date())
|
||||||
|
} catch {
|
||||||
|
setRows([])
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRows([])
|
||||||
|
fetchLeaderboard(activeTab)
|
||||||
|
const id = setInterval(() => fetchLeaderboard(activeTab), 30_000)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}, [activeTab, fetchLeaderboard])
|
||||||
|
|
||||||
|
const currentTab = TABS.find(t => t.key === activeTab)
|
||||||
|
|
||||||
|
const medalColor = (i) => {
|
||||||
|
if (i === 0) return '#f39c12'
|
||||||
|
if (i === 1) return '#9b9b9b'
|
||||||
|
if (i === 2) return '#cd7f32'
|
||||||
|
return theme.accentLight
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen pt-20 px-4">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-4xl font-black text-rolld-text mb-2">Classements</h1>
|
||||||
|
<p className="text-rolld-muted text-sm">Top 10 joueurs par catégorie — mis à jour en temps réel</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex flex-wrap gap-2 justify-center mb-8">
|
||||||
|
{TABS.map(tab => (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
onClick={() => setActiveTab(tab.key)}
|
||||||
|
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 ${
|
||||||
|
activeTab === tab.key
|
||||||
|
? 'text-white'
|
||||||
|
: 'text-rolld-muted hover:text-rolld-text bg-rolld-surface border border-rolld-border'
|
||||||
|
}`}
|
||||||
|
style={activeTab === tab.key ? {
|
||||||
|
background: `linear-gradient(to right, ${theme.accent}, ${theme.gradientTo})`,
|
||||||
|
} : {}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="rounded-2xl overflow-hidden border border-rolld-border bg-rolld-surface">
|
||||||
|
<div className="px-6 py-4 border-b border-rolld-border flex items-center justify-between">
|
||||||
|
<span className="text-rolld-text font-semibold">{currentTab.label}</span>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{lastRefresh && (
|
||||||
|
<span className="text-rolld-muted text-xs">
|
||||||
|
{lastRefresh.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => fetchLeaderboard(activeTab)}
|
||||||
|
disabled={loading}
|
||||||
|
className="text-xs text-rolld-accent hover:text-rolld-accent-light transition-colors disabled:opacity-40"
|
||||||
|
>
|
||||||
|
{loading ? '...' : 'Actualiser'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && rows.length === 0 ? (
|
||||||
|
<div className="py-16 text-center text-rolld-muted text-sm">Chargement…</div>
|
||||||
|
) : rows.length === 0 ? (
|
||||||
|
<div className="py-16 text-center text-rolld-muted text-sm">Aucune donnée pour l'instant.</div>
|
||||||
|
) : (
|
||||||
|
<table className="w-full">
|
||||||
|
<tbody>
|
||||||
|
{rows.map((row, i) => (
|
||||||
|
<tr
|
||||||
|
key={row.name}
|
||||||
|
className={`border-b border-rolld-border/50 last:border-0 transition-colors hover:bg-rolld-border/20 ${
|
||||||
|
i === 0 ? 'bg-rolld-border/10' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<td className="px-6 py-4 w-12">
|
||||||
|
<span className="text-sm font-bold" style={{ color: medalColor(i) }}>
|
||||||
|
{i < 3 ? ['🥇', '🥈', '🥉'][i] : `#${i + 1}`}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-4 flex-1 text-rolld-text font-medium">
|
||||||
|
{row.name}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-right font-mono text-sm" style={{ color: theme.accentLight }}>
|
||||||
|
{currentTab.format(row.value)}
|
||||||
|
{currentTab.unit && <span className="text-rolld-muted ml-1">{currentTab.unit}</span>}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!134 &13400000
|
|
||||||
PhysicsMaterial:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_Name: Bouncy
|
|
||||||
serializedVersion: 2
|
|
||||||
m_DynamicFriction: 0.6
|
|
||||||
m_StaticFriction: 0.6
|
|
||||||
m_Bounciness: 0.74
|
|
||||||
m_FrictionCombine: 0
|
|
||||||
m_BounceCombine: 0
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 38ed95051af515848a7513429d4f0413
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 13400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!134 &13400000
|
|
||||||
PhysicsMaterial:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_Name: GelBleu
|
|
||||||
serializedVersion: 2
|
|
||||||
m_DynamicFriction: 0
|
|
||||||
m_StaticFriction: 0
|
|
||||||
m_Bounciness: 1
|
|
||||||
m_FrictionCombine: 1
|
|
||||||
m_BounceCombine: 3
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 458e6466a22c1204cb2e77d378867d7b
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 13400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!134 &13400000
|
|
||||||
PhysicsMaterial:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_Name: GelOrange
|
|
||||||
serializedVersion: 2
|
|
||||||
m_DynamicFriction: 0.6
|
|
||||||
m_StaticFriction: 0.6
|
|
||||||
m_Bounciness: 0.74
|
|
||||||
m_FrictionCombine: 0
|
|
||||||
m_BounceCombine: 0
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 61512ca9473715648874e2d1f555c50f
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 13400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!134 &13400000
|
|
||||||
PhysicsMaterial:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_Name: GelViolet
|
|
||||||
serializedVersion: 2
|
|
||||||
m_DynamicFriction: 1
|
|
||||||
m_StaticFriction: 1
|
|
||||||
m_Bounciness: 0
|
|
||||||
m_FrictionCombine: 3
|
|
||||||
m_BounceCombine: 0
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 86c56232f118b4c4caa7fc9d124fc344
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 0
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -8,8 +8,8 @@ Material:
|
|||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_Name: CheckpointMat
|
m_Name: CheckpointMat
|
||||||
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
|
m_Shader: {fileID: -6465566751694194690, guid: c52a5eb90c085474582a223ce9475866, type: 3}
|
||||||
m_Parent: {fileID: 0}
|
m_Parent: {fileID: -876546973899608171, guid: c52a5eb90c085474582a223ce9475866, type: 3}
|
||||||
m_ModifiedSerializedProperties: 0
|
m_ModifiedSerializedProperties: 0
|
||||||
m_ValidKeywords: []
|
m_ValidKeywords: []
|
||||||
m_InvalidKeywords: []
|
m_InvalidKeywords: []
|
||||||
@@ -22,63 +22,22 @@ Material:
|
|||||||
m_LockedProperties:
|
m_LockedProperties:
|
||||||
m_SavedProperties:
|
m_SavedProperties:
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_TexEnvs:
|
m_TexEnvs: []
|
||||||
- _BumpMap:
|
|
||||||
m_Texture: {fileID: 0}
|
|
||||||
m_Scale: {x: 1, y: 1}
|
|
||||||
m_Offset: {x: 0, y: 0}
|
|
||||||
- _DetailAlbedoMap:
|
|
||||||
m_Texture: {fileID: 0}
|
|
||||||
m_Scale: {x: 1, y: 1}
|
|
||||||
m_Offset: {x: 0, y: 0}
|
|
||||||
- _DetailMask:
|
|
||||||
m_Texture: {fileID: 0}
|
|
||||||
m_Scale: {x: 1, y: 1}
|
|
||||||
m_Offset: {x: 0, y: 0}
|
|
||||||
- _DetailNormalMap:
|
|
||||||
m_Texture: {fileID: 0}
|
|
||||||
m_Scale: {x: 1, y: 1}
|
|
||||||
m_Offset: {x: 0, y: 0}
|
|
||||||
- _EmissionMap:
|
|
||||||
m_Texture: {fileID: 0}
|
|
||||||
m_Scale: {x: 1, y: 1}
|
|
||||||
m_Offset: {x: 0, y: 0}
|
|
||||||
- _MainTex:
|
|
||||||
m_Texture: {fileID: 0}
|
|
||||||
m_Scale: {x: 1, y: 1}
|
|
||||||
m_Offset: {x: 0, y: 0}
|
|
||||||
- _MetallicGlossMap:
|
|
||||||
m_Texture: {fileID: 0}
|
|
||||||
m_Scale: {x: 1, y: 1}
|
|
||||||
m_Offset: {x: 0, y: 0}
|
|
||||||
- _OcclusionMap:
|
|
||||||
m_Texture: {fileID: 0}
|
|
||||||
m_Scale: {x: 1, y: 1}
|
|
||||||
m_Offset: {x: 0, y: 0}
|
|
||||||
- _ParallaxMap:
|
|
||||||
m_Texture: {fileID: 0}
|
|
||||||
m_Scale: {x: 1, y: 1}
|
|
||||||
m_Offset: {x: 0, y: 0}
|
|
||||||
m_Ints: []
|
m_Ints: []
|
||||||
m_Floats:
|
m_Floats: []
|
||||||
- _BumpScale: 1
|
m_Colors: []
|
||||||
- _Cutoff: 0.5
|
|
||||||
- _DetailNormalMapScale: 1
|
|
||||||
- _DstBlend: 0
|
|
||||||
- _GlossMapScale: 1
|
|
||||||
- _Glossiness: 0.5
|
|
||||||
- _GlossyReflections: 1
|
|
||||||
- _Metallic: 0
|
|
||||||
- _Mode: 0
|
|
||||||
- _OcclusionStrength: 1
|
|
||||||
- _Parallax: 0.02
|
|
||||||
- _SmoothnessTextureChannel: 0
|
|
||||||
- _SpecularHighlights: 1
|
|
||||||
- _SrcBlend: 1
|
|
||||||
- _UVSec: 0
|
|
||||||
- _ZWrite: 1
|
|
||||||
m_Colors:
|
|
||||||
- _Color: {r: 0.1, g: 0.9, b: 0.3, a: 1}
|
|
||||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
|
||||||
m_BuildTextureStacks: []
|
m_BuildTextureStacks: []
|
||||||
m_AllowLocking: 1
|
m_AllowLocking: 1
|
||||||
|
--- !u!114 &8924139153543123182
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 11
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Unity.ShaderGraph.Editor::UnityEditor.Rendering.BuiltIn.AssetVersion
|
||||||
|
version: 0
|
||||||
|
|||||||
Binary file not shown.
@@ -1,15 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!134 &13400000
|
|
||||||
PhysicsMaterial:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_Name: Normal
|
|
||||||
serializedVersion: 2
|
|
||||||
m_DynamicFriction: 0.6
|
|
||||||
m_StaticFriction: 0.6
|
|
||||||
m_Bounciness: 0
|
|
||||||
m_FrictionCombine: 0
|
|
||||||
m_BounceCombine: 0
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 36e82e5cf5450404999af634c1d3cbbd
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 13400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -20,9 +20,9 @@ public class PlayerController : MonoBehaviour
|
|||||||
|
|
||||||
[Header("Steering Feel")]
|
[Header("Steering Feel")]
|
||||||
[Tooltip("Damps velocity perpendicular to input — higher = sharper turns")]
|
[Tooltip("Damps velocity perpendicular to input — higher = sharper turns")]
|
||||||
public float turnDamping = 7f;
|
public float turnDamping = 1.5f;
|
||||||
[Tooltip("Horizontal friction when no input is held")]
|
[Tooltip("Horizontal friction when no input is held")]
|
||||||
public float idleDrag = 3f;
|
public float idleDrag = 0.2f;
|
||||||
|
|
||||||
[Header("Bump Collision")]
|
[Header("Bump Collision")]
|
||||||
public float bumpForce = 4f; // Impulse force when bumping a remote player
|
public float bumpForce = 4f; // Impulse force when bumping a remote player
|
||||||
@@ -44,6 +44,7 @@ public class PlayerController : MonoBehaviour
|
|||||||
public float maxVelocity = 120f; // Velocity cap to prevent infinite acceleration
|
public float maxVelocity = 120f; // Velocity cap to prevent infinite acceleration
|
||||||
public float respawnY = -10f; // Y threshold for respawn
|
public float respawnY = -10f; // Y threshold for respawn
|
||||||
private Vector3 _spawnPos = new Vector3(0f, 3f, -30f);
|
private Vector3 _spawnPos = new Vector3(0f, 3f, -30f);
|
||||||
|
private Rigidbody _rb;
|
||||||
|
|
||||||
// Squash & stretch
|
// Squash & stretch
|
||||||
private bool _isSquashing = false;
|
private bool _isSquashing = false;
|
||||||
@@ -80,10 +81,12 @@ public class PlayerController : MonoBehaviour
|
|||||||
void Start()
|
void Start()
|
||||||
{
|
{
|
||||||
Debug.Log("PlayerController script initialized.");
|
Debug.Log("PlayerController script initialized.");
|
||||||
// Cursor lock is handled by LobbyUI on connect/disconnect
|
_rb = GetComponent<Rigidbody>();
|
||||||
_meshTransform = transform; // Will be the sphere itself for squash
|
_meshTransform = transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetSpawnPosition(Vector3 pos) => _spawnPos = pos;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called by LobbyUI after connecting. Sets up the local player
|
/// Called by LobbyUI after connecting. Sets up the local player
|
||||||
/// with a floating name label and a 50% color tint.
|
/// with a floating name label and a 50% color tint.
|
||||||
@@ -166,8 +169,7 @@ public class PlayerController : MonoBehaviour
|
|||||||
var cam = Camera.main;
|
var cam = Camera.main;
|
||||||
if (cam != null)
|
if (cam != null)
|
||||||
{
|
{
|
||||||
// Billboard locked to Y axis — only rotate around vertical
|
Vector3 lookDir = cam.transform.position - _nameLabelObj.transform.position;
|
||||||
Vector3 lookDir = _nameLabelObj.transform.position - cam.transform.position;
|
|
||||||
lookDir.y = 0f;
|
lookDir.y = 0f;
|
||||||
if (lookDir.sqrMagnitude > 0.001f)
|
if (lookDir.sqrMagnitude > 0.001f)
|
||||||
_nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir);
|
_nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir);
|
||||||
@@ -178,20 +180,18 @@ public class PlayerController : MonoBehaviour
|
|||||||
// Update is called once per frame
|
// Update is called once per frame
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
// Toggle cursor lock/unlock avec clic droit (disabled when keybind menu is open)
|
// Cursor lock: right-click unlocks, left-click re-locks (disabled when any UI panel is open)
|
||||||
if (!KeyBindingUI.IsVisible && Mouse.current != null && Mouse.current.rightButton.wasPressedThisFrame)
|
if (!ChatUI.IsVisible && !KeyBindingUI.IsVisible && Mouse.current != null)
|
||||||
{
|
{
|
||||||
if (Cursor.lockState == CursorLockMode.Locked)
|
if (Cursor.lockState == CursorLockMode.Locked && Mouse.current.rightButton.wasPressedThisFrame)
|
||||||
{
|
{
|
||||||
Cursor.lockState = CursorLockMode.None;
|
Cursor.lockState = CursorLockMode.None;
|
||||||
Cursor.visible = true;
|
Cursor.visible = true;
|
||||||
Debug.Log("Cursor UNLOCKED");
|
|
||||||
}
|
}
|
||||||
else
|
else if (Cursor.lockState != CursorLockMode.Locked && Mouse.current.leftButton.wasPressedThisFrame)
|
||||||
{
|
{
|
||||||
Cursor.lockState = CursorLockMode.Locked;
|
Cursor.lockState = CursorLockMode.Locked;
|
||||||
Cursor.visible = false;
|
Cursor.visible = false;
|
||||||
Debug.Log("Cursor LOCKED");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,16 +208,15 @@ public class PlayerController : MonoBehaviour
|
|||||||
// --- Respawn if fallen off the map ---
|
// --- Respawn if fallen off the map ---
|
||||||
if (transform.position.y < respawnY)
|
if (transform.position.y < respawnY)
|
||||||
{
|
{
|
||||||
Rigidbody rbRespawn = GetComponent<Rigidbody>();
|
if (_rb != null)
|
||||||
if (rbRespawn != null)
|
|
||||||
{
|
{
|
||||||
rbRespawn.linearVelocity = Vector3.zero;
|
_rb.linearVelocity = Vector3.zero;
|
||||||
rbRespawn.angularVelocity = Vector3.zero;
|
_rb.angularVelocity = Vector3.zero;
|
||||||
|
_rb.useGravity = true;
|
||||||
}
|
}
|
||||||
transform.position = _spawnPos;
|
transform.position = _spawnPos;
|
||||||
isOnGelViolet = false;
|
isOnGelViolet = false;
|
||||||
isOnGelOrange = false;
|
isOnGelOrange = false;
|
||||||
if (rbRespawn != null) rbRespawn.useGravity = true;
|
|
||||||
Debug.Log("[Player] Respawned after falling.");
|
Debug.Log("[Player] Respawned after falling.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -227,7 +226,7 @@ public class PlayerController : MonoBehaviour
|
|||||||
_fallWarningAlpha = Mathf.Lerp(_fallWarningAlpha, fallTarget, Time.deltaTime * 5f);
|
_fallWarningAlpha = Mathf.Lerp(_fallWarningAlpha, fallTarget, Time.deltaTime * 5f);
|
||||||
|
|
||||||
// Mouvement continu selon les directions maintenues
|
// Mouvement continu selon les directions maintenues
|
||||||
Rigidbody rb = GetComponent<Rigidbody>();
|
Rigidbody rb = _rb;
|
||||||
if (rb != null)
|
if (rb != null)
|
||||||
{
|
{
|
||||||
float currentSpeed = MovementSpeed;
|
float currentSpeed = MovementSpeed;
|
||||||
@@ -326,42 +325,25 @@ public class PlayerController : MonoBehaviour
|
|||||||
void OnCollisionStay(Collision collision)
|
void OnCollisionStay(Collision collision)
|
||||||
{
|
{
|
||||||
Collider col = collision.collider;
|
Collider col = collision.collider;
|
||||||
if (col != null && col.sharedMaterial != null)
|
if (col == null || col.sharedMaterial == null) return;
|
||||||
{
|
|
||||||
if (col.sharedMaterial.name.Contains("GelOrange"))
|
|
||||||
{
|
|
||||||
isOnGelOrange = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
isOnGelOrange = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (col.sharedMaterial.name.Contains("GelViolet"))
|
if (col.sharedMaterial.name.Contains("GelOrange"))
|
||||||
|
{
|
||||||
|
isOnGelOrange = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (col.sharedMaterial.name.Contains("GelViolet"))
|
||||||
|
{
|
||||||
|
if (!isOnGelViolet)
|
||||||
{
|
{
|
||||||
if (!isOnGelViolet)
|
originalDrag = _rb != null ? _rb.linearDamping : 0f;
|
||||||
{
|
if (_rb != null) _rb.linearDamping = 1f;
|
||||||
// Premier contact : sauvegarder le drag et augmenter le frein
|
|
||||||
Rigidbody rb = GetComponent<Rigidbody>();
|
|
||||||
if (rb != null)
|
|
||||||
{
|
|
||||||
originalDrag = rb.linearDamping;
|
|
||||||
rb.linearDamping = 1f; // Fort frein pour éviter le catapultage aux bords
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isOnGelViolet = true;
|
|
||||||
// Normale instantanée de la surface de contact
|
|
||||||
Vector3 avgNormal = Vector3.zero;
|
|
||||||
foreach (ContactPoint contact in collision.contacts)
|
|
||||||
{
|
|
||||||
avgNormal += contact.normal;
|
|
||||||
}
|
|
||||||
stickyNormal = avgNormal.normalized;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
isOnGelViolet = false;
|
|
||||||
}
|
}
|
||||||
|
isOnGelViolet = true;
|
||||||
|
Vector3 avgNormal = Vector3.zero;
|
||||||
|
foreach (ContactPoint contact in collision.contacts)
|
||||||
|
avgNormal += contact.normal;
|
||||||
|
stickyNormal = avgNormal.normalized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,33 +372,19 @@ public class PlayerController : MonoBehaviour
|
|||||||
|
|
||||||
public void OnJump(InputAction.CallbackContext context)
|
public void OnJump(InputAction.CallbackContext context)
|
||||||
{
|
{
|
||||||
|
if (ChatUI.IsVisible) { isJumpPressed = false; jumpPressTime = 0f; return; }
|
||||||
|
|
||||||
if (context.started)
|
if (context.started)
|
||||||
{
|
{
|
||||||
// Touche appuyée
|
|
||||||
isJumpPressed = true;
|
isJumpPressed = true;
|
||||||
jumpPressTime = 0f;
|
jumpPressTime = 0f;
|
||||||
Debug.Log("Jump Started");
|
StatsTracker.Instance?.RegisterJump();
|
||||||
}
|
|
||||||
else if (context.performed)
|
|
||||||
{
|
|
||||||
// Action validée (utile pour saut immédiat aussi)
|
|
||||||
Debug.Log("Jump Performed");
|
|
||||||
}
|
}
|
||||||
else if (context.canceled)
|
else if (context.canceled)
|
||||||
{
|
{
|
||||||
// Touche relâchée
|
|
||||||
float jumpForceFactor = Mathf.Clamp01(jumpPressTime / maxJumpHoldTime);
|
float jumpForceFactor = Mathf.Clamp01(jumpPressTime / maxJumpHoldTime);
|
||||||
if (IsGrounded())
|
if (IsGrounded())
|
||||||
{
|
|
||||||
PerformJump(jumpForceFactor * JumpForce);
|
PerformJump(jumpForceFactor * JumpForce);
|
||||||
Debug.Log($"Jump Released after {jumpPressTime}s -> Force factor: {jumpForceFactor}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.Log("Jump Released but not grounded.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset jump state so gauge goes back to 0
|
|
||||||
isJumpPressed = false;
|
isJumpPressed = false;
|
||||||
jumpPressTime = 0f;
|
jumpPressTime = 0f;
|
||||||
}
|
}
|
||||||
@@ -424,17 +392,9 @@ public class PlayerController : MonoBehaviour
|
|||||||
|
|
||||||
public void PerformJump(float force)
|
public void PerformJump(float force)
|
||||||
{
|
{
|
||||||
Rigidbody rb = GetComponent<Rigidbody>();
|
if (_rb == null) return;
|
||||||
if (rb != null)
|
Vector3 jumpDir = isOnGelViolet ? stickyNormal : Vector3.up;
|
||||||
{
|
_rb.AddForce(jumpDir * force, ForceMode.Impulse);
|
||||||
// Jump direction: surface normal when on sticky, otherwise up
|
|
||||||
Vector3 jumpDir = isOnGelViolet ? stickyNormal : Vector3.up;
|
|
||||||
rb.AddForce(jumpDir * force, ForceMode.Impulse);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogWarning("Rigidbody component not found on PlayerController.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsGrounded()
|
private bool IsGrounded()
|
||||||
@@ -448,75 +408,30 @@ public class PlayerController : MonoBehaviour
|
|||||||
|
|
||||||
public void OnForward(InputAction.CallbackContext context)
|
public void OnForward(InputAction.CallbackContext context)
|
||||||
{
|
{
|
||||||
if (context.started)
|
if (ChatUI.IsVisible) { isForwardHeld = false; return; }
|
||||||
{
|
if (context.started) isForwardHeld = true;
|
||||||
isForwardHeld = true;
|
else if (context.canceled) isForwardHeld = false;
|
||||||
Debug.Log("Forward Action Started");
|
|
||||||
}
|
|
||||||
else if (context.performed)
|
|
||||||
{
|
|
||||||
// Forward action performed
|
|
||||||
Debug.Log("Forward Action Performed");
|
|
||||||
}
|
|
||||||
else if (context.canceled)
|
|
||||||
{
|
|
||||||
isForwardHeld = false;
|
|
||||||
Debug.Log("Forward Action Canceled");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnBackwards(InputAction.CallbackContext context)
|
public void OnBackwards(InputAction.CallbackContext context)
|
||||||
{
|
{
|
||||||
if (context.started)
|
if (ChatUI.IsVisible) { isBackwardsHeld = false; return; }
|
||||||
{
|
if (context.started) isBackwardsHeld = true;
|
||||||
isBackwardsHeld = true;
|
else if (context.canceled) isBackwardsHeld = false;
|
||||||
Debug.Log("Backwards Action Started");
|
|
||||||
}
|
|
||||||
else if (context.performed)
|
|
||||||
{
|
|
||||||
Debug.Log("Backwards Action Performed");
|
|
||||||
}
|
|
||||||
else if (context.canceled)
|
|
||||||
{
|
|
||||||
isBackwardsHeld = false;
|
|
||||||
Debug.Log("Backwards Action Canceled");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnLeft(InputAction.CallbackContext context)
|
public void OnLeft(InputAction.CallbackContext context)
|
||||||
{
|
{
|
||||||
if (context.started)
|
if (ChatUI.IsVisible) { isLeftHeld = false; return; }
|
||||||
{
|
if (context.started) isLeftHeld = true;
|
||||||
isLeftHeld = true;
|
else if (context.canceled) isLeftHeld = false;
|
||||||
Debug.Log("Left Action Started");
|
|
||||||
}
|
|
||||||
else if (context.performed)
|
|
||||||
{
|
|
||||||
Debug.Log("Left Action Performed");
|
|
||||||
}
|
|
||||||
else if (context.canceled)
|
|
||||||
{
|
|
||||||
isLeftHeld = false;
|
|
||||||
Debug.Log("Left Action Canceled");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnRight(InputAction.CallbackContext context)
|
public void OnRight(InputAction.CallbackContext context)
|
||||||
{
|
{
|
||||||
if (context.started)
|
if (ChatUI.IsVisible) { isRightHeld = false; return; }
|
||||||
{
|
if (context.started) isRightHeld = true;
|
||||||
isRightHeld = true;
|
else if (context.canceled) isRightHeld = false;
|
||||||
Debug.Log("Right Action Started");
|
|
||||||
}
|
|
||||||
else if (context.performed)
|
|
||||||
{
|
|
||||||
Debug.Log("Right Action Performed");
|
|
||||||
}
|
|
||||||
else if (context.canceled)
|
|
||||||
{
|
|
||||||
isRightHeld = false;
|
|
||||||
Debug.Log("Right Action Canceled");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Bump collision with remote players ---
|
// --- Bump collision with remote players ---
|
||||||
@@ -540,17 +455,15 @@ public class PlayerController : MonoBehaviour
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
_lastBumpTime[id] = Time.time;
|
_lastBumpTime[id] = Time.time;
|
||||||
|
StatsTracker.Instance?.RegisterBump();
|
||||||
|
|
||||||
// Repulsion direction: from remote toward local player
|
// Repulsion direction: from remote toward local player
|
||||||
Vector3 dir = (transform.position - other.transform.position).normalized;
|
Vector3 dir = (transform.position - other.transform.position).normalized;
|
||||||
// Add slight upward component so the ball lifts off the ground
|
// Add slight upward component so the ball lifts off the ground
|
||||||
dir = (dir + Vector3.up * 0.3f).normalized;
|
dir = (dir + Vector3.up * 0.3f).normalized;
|
||||||
|
|
||||||
var rb = GetComponent<Rigidbody>();
|
if (_rb != null)
|
||||||
if (rb != null)
|
_rb.AddForce(dir * bumpForce, ForceMode.Impulse);
|
||||||
{
|
|
||||||
rb.AddForce(dir * bumpForce, ForceMode.Impulse);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnCollisionEnter(Collision collision)
|
void OnCollisionEnter(Collision collision)
|
||||||
@@ -590,6 +503,16 @@ public class PlayerController : MonoBehaviour
|
|||||||
_isSquashing = false;
|
_isSquashing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ResetInputs()
|
||||||
|
{
|
||||||
|
isForwardHeld = false;
|
||||||
|
isBackwardsHeld = false;
|
||||||
|
isLeftHeld = false;
|
||||||
|
isRightHeld = false;
|
||||||
|
isJumpPressed = false;
|
||||||
|
jumpPressTime = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
void OnDestroy()
|
void OnDestroy()
|
||||||
{
|
{
|
||||||
// Clean up name label (it's not parented to the ball)
|
// Clean up name label (it's not parented to the ball)
|
||||||
@@ -633,6 +556,13 @@ public class PlayerController : MonoBehaviour
|
|||||||
fontStyle = FontStyle.Bold
|
fontStyle = FontStyle.Bold
|
||||||
};
|
};
|
||||||
labelStyle.normal.textColor = new Color(1f, 1f, 1f, _gaugeDisplayAlpha * 0.9f);
|
labelStyle.normal.textColor = new Color(1f, 1f, 1f, _gaugeDisplayAlpha * 0.9f);
|
||||||
|
// Outline: draw 4× in black at ±1px, then once in white
|
||||||
|
var shadowStyle = new GUIStyle(labelStyle);
|
||||||
|
shadowStyle.normal.textColor = new Color(0f, 0f, 0f, _gaugeDisplayAlpha * 0.55f);
|
||||||
|
GUI.Label(new Rect(x + 1f, y - 25f, barWidth, 24f), "JUMP POWER", shadowStyle);
|
||||||
|
GUI.Label(new Rect(x - 1f, y - 27f, barWidth, 24f), "JUMP POWER", shadowStyle);
|
||||||
|
GUI.Label(new Rect(x + 1f, y - 27f, barWidth, 24f), "JUMP POWER", shadowStyle);
|
||||||
|
GUI.Label(new Rect(x - 1f, y - 25f, barWidth, 24f), "JUMP POWER", shadowStyle);
|
||||||
GUI.Label(new Rect(x, y - 26f, barWidth, 24f), "JUMP POWER", labelStyle);
|
GUI.Label(new Rect(x, y - 26f, barWidth, 24f), "JUMP POWER", labelStyle);
|
||||||
|
|
||||||
// Ensure textures
|
// Ensure textures
|
||||||
@@ -742,10 +672,9 @@ public class PlayerController : MonoBehaviour
|
|||||||
// ========================
|
// ========================
|
||||||
// SPEED INDICATOR
|
// SPEED INDICATOR
|
||||||
// ========================
|
// ========================
|
||||||
Rigidbody rbHud = GetComponent<Rigidbody>();
|
if (_rb != null)
|
||||||
if (rbHud != null)
|
|
||||||
{
|
{
|
||||||
float speed = rbHud.linearVelocity.magnitude;
|
float speed = _rb.linearVelocity.magnitude;
|
||||||
var speedStyle = new GUIStyle(GUI.skin.label)
|
var speedStyle = new GUIStyle(GUI.skin.label)
|
||||||
{
|
{
|
||||||
alignment = TextAnchor.MiddleRight,
|
alignment = TextAnchor.MiddleRight,
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!114 &-670956545734759409
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 3
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: b94fcd11afffcb142908bfcb1e261fba, type: 3}
|
|
||||||
m_Name: MotionBlur
|
|
||||||
m_EditorClassIdentifier: Unity.Postprocessing.Runtime::UnityEngine.Rendering.PostProcessing.MotionBlur
|
|
||||||
active: 1
|
|
||||||
enabled:
|
|
||||||
overrideState: 1
|
|
||||||
value: 1
|
|
||||||
shutterAngle:
|
|
||||||
overrideState: 0
|
|
||||||
value: 270
|
|
||||||
sampleCount:
|
|
||||||
overrideState: 0
|
|
||||||
value: 10
|
|
||||||
--- !u!114 &11400000
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 8e6292b2c06870d4495f009f912b9600, type: 3}
|
|
||||||
m_Name: PostProcessing Profile
|
|
||||||
m_EditorClassIdentifier: Unity.Postprocessing.Runtime::UnityEngine.Rendering.PostProcessing.PostProcessProfile
|
|
||||||
settings:
|
|
||||||
- {fileID: -670956545734759409}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: fc446179a9ae97a4a8ad5c8aa1c2dd47
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 11400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!114 &11400000
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: fcf7219bab7fe46a1ad266029b2fee19, type: 3}
|
|
||||||
m_Name: Readme
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
icon: {fileID: 2800000, guid: 727a75301c3d24613a3ebcec4a24c2c8, type: 3}
|
|
||||||
title: URP Empty Template
|
|
||||||
sections:
|
|
||||||
- heading: Welcome to the Universal Render Pipeline
|
|
||||||
text: This template includes the settings and assets you need to start creating with the Universal Render Pipeline.
|
|
||||||
linkText:
|
|
||||||
url:
|
|
||||||
- heading: URP Documentation
|
|
||||||
text:
|
|
||||||
linkText: Read more about URP
|
|
||||||
url: https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@latest
|
|
||||||
- heading: Forums
|
|
||||||
text:
|
|
||||||
linkText: Get answers and support
|
|
||||||
url: https://forum.unity.com/forums/universal-render-pipeline.383/
|
|
||||||
- heading: Report bugs
|
|
||||||
text:
|
|
||||||
linkText: Submit a report
|
|
||||||
url: https://unity3d.com/unity/qa/bug-reporting
|
|
||||||
loadedLayout: 1
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 8105016687592461f977c054a80ce2f2
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 0
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -10,6 +10,6 @@ PhysicsMaterial:
|
|||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_DynamicFriction: 0.6
|
m_DynamicFriction: 0.6
|
||||||
m_StaticFriction: 0.6
|
m_StaticFriction: 0.6
|
||||||
m_Bounciness: 0
|
m_Bounciness: 0.2
|
||||||
m_FrictionCombine: 0
|
m_FrictionCombine: 0
|
||||||
m_BounceCombine: 0
|
m_BounceCombine: 0
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,573 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds a compact 80×80 tutorial arena at runtime with connected zones:
|
|
||||||
///
|
|
||||||
/// Zone A (South) — Movement basics + jump gaps
|
|
||||||
/// Zone B (Center-S) — Bounce (GelBleu) training
|
|
||||||
/// Zone C (West) — Speed (GelOrange) training
|
|
||||||
/// Zone D (NW) — Moving platforms
|
|
||||||
/// Zone E (NE) — Sticky walls (GelViolet) training
|
|
||||||
/// Zone F (North) — Combo challenge (all gels + platforms)
|
|
||||||
/// Zone G (Center) — Central tower (final challenge)
|
|
||||||
///
|
|
||||||
/// Spawn: (0, 2, -30) — south edge
|
|
||||||
///
|
|
||||||
/// Ball capabilities reference (from PlayerController):
|
|
||||||
/// Max jump height ≈ 5.1 units (JumpForce=10, gravity=9.81)
|
|
||||||
/// Horizontal jump range ≈ 6-8 units at cruising speed
|
|
||||||
/// GelBleu bounce = perfect elasticity (bounciness 1.0, combine=Max)
|
|
||||||
/// GelOrange speed = ×3.83 multiplier (~19 u/s)
|
|
||||||
/// GelViolet sticky = surface-relative movement, jump = surface normal
|
|
||||||
/// Ball radius = 0.5, min platform 2×2 for playability
|
|
||||||
///
|
|
||||||
/// Loads PhysicMaterials from Resources/ folder.
|
|
||||||
/// Attach to a persistent GameObject (e.g. NetworkManager).
|
|
||||||
/// </summary>
|
|
||||||
public class ArenaZoneBuilder : MonoBehaviour
|
|
||||||
{
|
|
||||||
private PhysicsMaterial _matGelBleu;
|
|
||||||
private PhysicsMaterial _matGelOrange;
|
|
||||||
private PhysicsMaterial _matGelViolet;
|
|
||||||
private PhysicsMaterial _matBouncy;
|
|
||||||
private PhysicsMaterial _matNormal;
|
|
||||||
private Material _baseMat;
|
|
||||||
|
|
||||||
private readonly List<MovingPlatform> _movingPlatforms = new List<MovingPlatform>();
|
|
||||||
|
|
||||||
// ── Color palette ──
|
|
||||||
private static readonly Color ColFloor = new Color(0.28f, 0.29f, 0.34f, 1f);
|
|
||||||
private static readonly Color ColBleu = new Color(0.2f, 0.5f, 1f, 0.85f);
|
|
||||||
private static readonly Color ColBleuLt = new Color(0.3f, 0.65f, 1f, 0.9f);
|
|
||||||
private static readonly Color ColOrange = new Color(1f, 0.55f, 0.1f, 0.85f);
|
|
||||||
private static readonly Color ColOrangeLt = new Color(1f, 0.65f, 0.2f, 0.9f);
|
|
||||||
private static readonly Color ColViolet = new Color(0.6f, 0.2f, 0.8f, 0.8f);
|
|
||||||
private static readonly Color ColVioletLt = new Color(0.7f, 0.35f, 0.9f, 0.85f);
|
|
||||||
private static readonly Color ColNormal = new Color(0.42f, 0.43f, 0.50f, 0.9f);
|
|
||||||
private static readonly Color ColNormalLt = new Color(0.52f, 0.53f, 0.60f, 0.95f);
|
|
||||||
private static readonly Color ColDark = new Color(0.22f, 0.22f, 0.28f, 0.95f);
|
|
||||||
private static readonly Color ColGold = new Color(1f, 0.84f, 0f, 0.95f);
|
|
||||||
private static readonly Color ColWall = new Color(0.35f, 0.35f, 0.42f, 0.95f);
|
|
||||||
private static readonly Color ColPath = new Color(0.38f, 0.40f, 0.48f, 0.9f);
|
|
||||||
private static readonly Color ColSignBleu = new Color(0.15f, 0.4f, 0.9f, 0.95f);
|
|
||||||
private static readonly Color ColSignOrange= new Color(0.9f, 0.45f, 0.05f, 0.95f);
|
|
||||||
private static readonly Color ColSignViolet= new Color(0.5f, 0.15f, 0.7f, 0.95f);
|
|
||||||
private static readonly Color ColSignGrey = new Color(0.5f, 0.5f, 0.55f, 0.95f);
|
|
||||||
private static readonly Color ColSignGold = new Color(0.9f, 0.75f, 0f, 0.95f);
|
|
||||||
private static readonly Color ColGuide = new Color(0.5f, 0.5f, 0.6f, 0.5f);
|
|
||||||
|
|
||||||
// ── Constants ──
|
|
||||||
private const float HALF = 40f; // Arena half-size
|
|
||||||
private const float WALL_H = 12f; // Perimeter wall height
|
|
||||||
private const float WALL_T = 1f; // Perimeter wall thickness
|
|
||||||
|
|
||||||
void Start()
|
|
||||||
{
|
|
||||||
LoadMaterials();
|
|
||||||
if (_baseMat == null) FindBaseMaterial();
|
|
||||||
|
|
||||||
BuildFloorAndWalls();
|
|
||||||
BuildPaths();
|
|
||||||
BuildZoneA_Movement();
|
|
||||||
BuildZoneB_Bounce();
|
|
||||||
BuildZoneC_Speed();
|
|
||||||
BuildZoneD_MovingPlatforms();
|
|
||||||
BuildZoneE_StickyWalls();
|
|
||||||
BuildZoneF_Combo();
|
|
||||||
BuildZoneG_CentralTower();
|
|
||||||
|
|
||||||
Debug.Log("[ArenaZoneBuilder] 80x80 tutorial arena built successfully.");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Update()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _movingPlatforms.Count; i++)
|
|
||||||
{
|
|
||||||
var mp = _movingPlatforms[i];
|
|
||||||
if (mp.go == null) continue;
|
|
||||||
mp.t += Time.deltaTime * mp.speed;
|
|
||||||
float ping = Mathf.PingPong(mp.t, 1f);
|
|
||||||
mp.go.transform.position = Vector3.Lerp(mp.posA, mp.posB, ping);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnDestroy()
|
|
||||||
{
|
|
||||||
_movingPlatforms.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// FLOOR, WALLS & PATHS
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private void BuildFloorAndWalls()
|
|
||||||
{
|
|
||||||
// Main floor
|
|
||||||
CreateZone("Arena_Floor", new Vector3(0, -0.25f, 0),
|
|
||||||
new Vector3(HALF * 2f, 0.5f, HALF * 2f), _matNormal, ColFloor);
|
|
||||||
|
|
||||||
// Perimeter walls (high to prevent escape)
|
|
||||||
float h2 = WALL_H / 2f;
|
|
||||||
float full = HALF * 2f + 2f;
|
|
||||||
CreateZone("Wall_N", new Vector3(0, h2, HALF), new Vector3(full, WALL_H, WALL_T), _matNormal, ColWall);
|
|
||||||
CreateZone("Wall_S", new Vector3(0, h2, -HALF), new Vector3(full, WALL_H, WALL_T), _matNormal, ColWall);
|
|
||||||
CreateZone("Wall_E", new Vector3(HALF, h2, 0), new Vector3(WALL_T, WALL_H, full), _matNormal, ColWall);
|
|
||||||
CreateZone("Wall_W", new Vector3(-HALF, h2, 0), new Vector3(WALL_T, WALL_H, full), _matNormal, ColWall);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Ground-level paths connecting all zones, with directional color markers.</summary>
|
|
||||||
private void BuildPaths()
|
|
||||||
{
|
|
||||||
float pathH = 0.06f;
|
|
||||||
float pathY = 0.03f;
|
|
||||||
|
|
||||||
// Spawn -> Zone A (already at spawn)
|
|
||||||
// Zone A -> Zone B (south to center-south)
|
|
||||||
CreateZone("Path_A_B", new Vector3(0, pathY, -22f), new Vector3(3f, pathH, 10f), _matNormal, ColPath);
|
|
||||||
// Zone B -> Zone G center
|
|
||||||
CreateZone("Path_B_G", new Vector3(0, pathY, -8f), new Vector3(3f, pathH, 10f), _matNormal, ColPath);
|
|
||||||
// Zone G -> Zone C (center to west)
|
|
||||||
CreateZone("Path_G_C", new Vector3(-10f, pathY, 0f), new Vector3(12f, pathH, 3f), _matNormal, ColPath);
|
|
||||||
// Zone C -> Zone D (west to northwest)
|
|
||||||
CreateZone("Path_C_D", new Vector3(-28f, pathY, 12f), new Vector3(3f, pathH, 16f), _matNormal, ColPath);
|
|
||||||
// Zone G -> Zone E (center to northeast)
|
|
||||||
CreateZone("Path_G_E", new Vector3(12f, pathY, 10f), new Vector3(16f, pathH, 3f), _matNormal, ColPath);
|
|
||||||
// Zone D/E -> Zone F (north)
|
|
||||||
CreateZone("Path_D_F", new Vector3(-12f, pathY, 28f), new Vector3(16f, pathH, 3f), _matNormal, ColPath);
|
|
||||||
CreateZone("Path_E_F", new Vector3(12f, pathY, 28f), new Vector3(16f, pathH, 3f), _matNormal, ColPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// ZONE A — MOVEMENT BASICS + JUMP GAPS (South)
|
|
||||||
// Origin: (0, 0, -32)
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private void BuildZoneA_Movement()
|
|
||||||
{
|
|
||||||
// Sign
|
|
||||||
CreateSign("Sign_A", new Vector3(-4f, 1.5f, -35f), ColSignGrey);
|
|
||||||
|
|
||||||
// Spawn platform (raised)
|
|
||||||
CreateZone("A_Spawn", new Vector3(0, 0.4f, -32f), new Vector3(8f, 0.8f, 6f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Straight corridor with slight turns to learn rolling
|
|
||||||
CreateZone("A_Corr1", new Vector3(0, 0.15f, -27f), new Vector3(4f, 0.3f, 5f), _matNormal, ColNormal);
|
|
||||||
CreateZone("A_Corr2", new Vector3(3f, 0.15f, -23f), new Vector3(4f, 0.3f, 4f), _matNormal, ColNormal);
|
|
||||||
CreateZone("A_Corr3", new Vector3(0f, 0.15f, -19.5f), new Vector3(4f, 0.3f, 4f), _matNormal, ColNormal);
|
|
||||||
|
|
||||||
// Low guide walls for the corridor
|
|
||||||
CreateZone("A_GuideL1", new Vector3(-2.5f, 0.6f, -27f), new Vector3(0.3f, 0.9f, 5f), _matNormal, ColGuide);
|
|
||||||
CreateZone("A_GuideR1", new Vector3(2.5f, 0.6f, -27f), new Vector3(0.3f, 0.9f, 5f), _matNormal, ColGuide);
|
|
||||||
|
|
||||||
// Jump gaps: 3 gaps of increasing difficulty
|
|
||||||
// Gap 1: 2 units gap
|
|
||||||
CreateZone("A_Plat1", new Vector3(-3f, 0.15f, -16f), new Vector3(4f, 0.3f, 4f), _matNormal, ColNormalLt);
|
|
||||||
// (gap of 2 units)
|
|
||||||
CreateZone("A_Plat2", new Vector3(-3f, 0.15f, -10f), new Vector3(4f, 0.3f, 4f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Gap 2: 3 units gap
|
|
||||||
// (gap of 3 units)
|
|
||||||
CreateZone("A_Plat3", new Vector3(-3f, 0.15f, -3f), new Vector3(4f, 0.3f, 4f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Gap 3: 4.5 units gap (needs charged jump)
|
|
||||||
// (gap of 4.5 units)
|
|
||||||
CreateZone("A_Plat4", new Vector3(-3f, 0.15f, 5f), new Vector3(4f, 0.3f, 4f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Return ramp to ground
|
|
||||||
CreateRamp("A_Ramp", new Vector3(-3f, 1.0f, 9f), new Vector3(4f, 0.3f, 5f),
|
|
||||||
-12f, Vector3.right, _matNormal, ColNormal);
|
|
||||||
|
|
||||||
Debug.Log("[ArenaZoneBuilder] Zone A (Movement) built.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// ZONE B — BOUNCE TRAINING (Center-South)
|
|
||||||
// Origin: (10, 0, -15)
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private void BuildZoneB_Bounce()
|
|
||||||
{
|
|
||||||
Vector3 o = new Vector3(10f, 0f, -15f);
|
|
||||||
|
|
||||||
// Sign
|
|
||||||
CreateSign("Sign_B", o + new Vector3(-3f, 1.5f, -3f), ColSignBleu);
|
|
||||||
|
|
||||||
// Entry pad
|
|
||||||
CreateZone("B_Entry", o + new Vector3(0, 0.15f, 0), new Vector3(5f, 0.3f, 5f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Bounce pads with targets at increasing heights
|
|
||||||
// Pad 1 (small) -> target at 3m
|
|
||||||
CreateZone("B_Pad1", o + new Vector3(0, 0.2f, 5f), new Vector3(3f, 0.25f, 3f), _matGelBleu, ColBleu);
|
|
||||||
CreateZone("B_Tgt1", o + new Vector3(0, 3f, 9f), new Vector3(4f, 0.4f, 4f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Pad 2 (medium) -> target at 4.5m
|
|
||||||
CreateZone("B_Pad2", o + new Vector3(0, 3.2f, 9f), new Vector3(3.5f, 0.25f, 3.5f), _matGelBleu, ColBleuLt);
|
|
||||||
CreateZone("B_Tgt2", o + new Vector3(4f, 5.5f, 12f), new Vector3(4f, 0.4f, 4f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Pad 3 on target 2 -> top at 8m
|
|
||||||
CreateZone("B_Pad3", o + new Vector3(4f, 5.7f, 12f), new Vector3(3f, 0.2f, 3f), _matGelBleu, ColBleu);
|
|
||||||
CreateZone("B_Tgt3", o + new Vector3(0, 8f, 15f), new Vector3(5f, 0.4f, 5f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Bounce staircase: fall from 8m -> bounce up further
|
|
||||||
CreateZone("B_Stair_Bnc", o + new Vector3(-5f, 0.2f, 15f), new Vector3(4f, 0.25f, 4f), _matGelBleu, ColBleu);
|
|
||||||
CreateZone("B_Stair_Mid", o + new Vector3(-5f, 5f, 19f), new Vector3(4f, 0.4f, 4f), _matNormal, ColNormalLt);
|
|
||||||
CreateZone("B_Stair_Top", o + new Vector3(-5f, 8.5f, 23f), new Vector3(5f, 0.4f, 4f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Return to ground via gentle ramp
|
|
||||||
CreateRamp("B_Return", o + new Vector3(-5f, 4f, 27f), new Vector3(4f, 0.3f, 8f),
|
|
||||||
-20f, Vector3.right, _matNormal, ColNormal);
|
|
||||||
|
|
||||||
Debug.Log("[ArenaZoneBuilder] Zone B (Bounce) built.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// ZONE C — SPEED TRAINING (West)
|
|
||||||
// Origin: (-25, 0, -5)
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private void BuildZoneC_Speed()
|
|
||||||
{
|
|
||||||
Vector3 o = new Vector3(-25f, 0f, -5f);
|
|
||||||
|
|
||||||
// Sign
|
|
||||||
CreateSign("Sign_C", o + new Vector3(8f, 1.5f, -2f), ColSignOrange);
|
|
||||||
|
|
||||||
// Entry
|
|
||||||
CreateZone("C_Entry", o + new Vector3(0, 0.15f, 0), new Vector3(5f, 0.3f, 5f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Speed strip 1 -> short, straight, with ramp launch
|
|
||||||
CreateZone("C_Strip1", o + new Vector3(0, 0.12f, 5f), new Vector3(4f, 0.15f, 8f), _matGelOrange, ColOrange);
|
|
||||||
// Guide walls
|
|
||||||
CreateZone("C_GuideL1", new Vector3(o.x - 2.5f, 0.5f, o.z + 5f), new Vector3(0.3f, 1f, 8f), _matNormal, ColGuide);
|
|
||||||
CreateZone("C_GuideR1", new Vector3(o.x + 2.5f, 0.5f, o.z + 5f), new Vector3(0.3f, 1f, 8f), _matNormal, ColGuide);
|
|
||||||
|
|
||||||
// Ramp at end of strip 1
|
|
||||||
CreateRamp("C_Ramp1", o + new Vector3(0, 0.8f, 10.5f), new Vector3(4f, 0.25f, 4f),
|
|
||||||
20f, Vector3.right, _matGelOrange, ColOrangeLt);
|
|
||||||
|
|
||||||
// Landing platform (12m ahead, 2m up)
|
|
||||||
CreateZone("C_Land1", o + new Vector3(0, 2f, 18f), new Vector3(5f, 0.4f, 5f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Speed strip 2 -> longer with a curve
|
|
||||||
CreateZone("C_Strip2a", o + new Vector3(0, 2.1f, 22f), new Vector3(4f, 0.15f, 5f), _matGelOrange, ColOrange);
|
|
||||||
CreateZone("C_Strip2b", o + new Vector3(4f, 2.1f, 26f), new Vector3(5f, 0.15f, 4f), _matGelOrange, ColOrangeLt);
|
|
||||||
CreateZone("C_Strip2c", o + new Vector3(8f, 2.1f, 30f), new Vector3(4f, 0.15f, 5f), _matGelOrange, ColOrange);
|
|
||||||
// Guide walls for curve
|
|
||||||
CreateZone("C_GuideO", o + new Vector3(-2.5f, 2.6f, 22f), new Vector3(0.3f, 1f, 5f), _matNormal, ColGuide);
|
|
||||||
CreateZone("C_GuideI", o + new Vector3(10.5f, 2.6f, 30f), new Vector3(0.3f, 1f, 5f), _matNormal, ColGuide);
|
|
||||||
|
|
||||||
// Final landing with another ramp
|
|
||||||
CreateRamp("C_Ramp2", o + new Vector3(8f, 3f, 34f), new Vector3(4f, 0.25f, 4f),
|
|
||||||
25f, Vector3.right, _matGelOrange, ColOrangeLt);
|
|
||||||
CreateZone("C_Land2", o + new Vector3(8f, 4.5f, 38f), new Vector3(6f, 0.4f, 5f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
Debug.Log("[ArenaZoneBuilder] Zone C (Speed) built.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// ZONE D — MOVING PLATFORMS (Northwest)
|
|
||||||
// Origin: (-28, 0, 18)
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private void BuildZoneD_MovingPlatforms()
|
|
||||||
{
|
|
||||||
Vector3 o = new Vector3(-28f, 0f, 18f);
|
|
||||||
|
|
||||||
// Sign
|
|
||||||
CreateSign("Sign_D", o + new Vector3(4f, 1.5f, -2f), ColSignGrey);
|
|
||||||
|
|
||||||
// Entry platform
|
|
||||||
CreateZone("D_Entry", o + new Vector3(0, 0.2f, 0), new Vector3(6f, 0.4f, 6f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// 3 horizontal sliders (oscillate along X, spaced along Z, rising)
|
|
||||||
for (int i = 0; i < 3; i++)
|
|
||||||
{
|
|
||||||
Vector3 a = o + new Vector3(-2f, 1.5f + i * 2f, 5f + i * 6f);
|
|
||||||
Vector3 b = a + new Vector3(8f, 0f, 0f);
|
|
||||||
var go = CreateZone("D_Slide" + i, a, new Vector3(4f, 0.5f, 4f), _matNormal, ColDark);
|
|
||||||
AddMovingPlatform(go, a, b, 0.3f + i * 0.1f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mid-platform
|
|
||||||
CreateZone("D_Mid", o + new Vector3(2f, 7f, 20f), new Vector3(5f, 0.4f, 5f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// 2 vertical lifts
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
Vector3 a = o + new Vector3(-4f + i * 8f, 7.5f, 24f + i * 5f);
|
|
||||||
Vector3 b = a + new Vector3(0f, 5f, 0f);
|
|
||||||
var go = CreateZone("D_Lift" + i, a, new Vector3(4f, 0.5f, 4f), _matNormal, ColDark);
|
|
||||||
AddMovingPlatform(go, a, b, 0.22f + i * 0.1f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// End platform (high, with view)
|
|
||||||
CreateZone("D_End", o + new Vector3(2f, 12f, 32f), new Vector3(6f, 0.4f, 6f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Bouncy descent pad at ground level
|
|
||||||
CreateZone("D_Desc", o + new Vector3(2f, 0.2f, 32f), new Vector3(4f, 0.25f, 4f), _matGelBleu, ColBleu);
|
|
||||||
|
|
||||||
Debug.Log("[ArenaZoneBuilder] Zone D (Moving Platforms) built.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// ZONE E — STICKY WALLS (Northeast)
|
|
||||||
// Origin: (24, 0, 15)
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private void BuildZoneE_StickyWalls()
|
|
||||||
{
|
|
||||||
Vector3 o = new Vector3(24f, 0f, 15f);
|
|
||||||
|
|
||||||
// Sign
|
|
||||||
CreateSign("Sign_E", o + new Vector3(-5f, 1.5f, -2f), ColSignViolet);
|
|
||||||
|
|
||||||
// Entry pad
|
|
||||||
CreateZone("E_Entry", o + new Vector3(0, 0.15f, 0), new Vector3(5f, 0.3f, 5f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// === Intro wall: simple vertical climb (4m high) ===
|
|
||||||
// Wall face pointing -X (ball approaches from the left)
|
|
||||||
CreateZone("E_Wall1", o + new Vector3(3f, 3f, 3f), new Vector3(0.5f, 6f, 5f), _matGelViolet, ColViolet);
|
|
||||||
// Platform at top of wall
|
|
||||||
CreateZone("E_Top1", o + new Vector3(3f, 6.2f, 3f), new Vector3(4f, 0.4f, 5f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// === L-shaped sticky: wall -> turn -> wall ===
|
|
||||||
// Vertical wall going up (face -Z)
|
|
||||||
CreateZone("E_Wall2a", o + new Vector3(0, 3.5f, 8f), new Vector3(4f, 7f, 0.5f), _matGelViolet, ColViolet);
|
|
||||||
// Ceiling connecting to second wall (face down, -Y)
|
|
||||||
CreateZone("E_Ceil", o + new Vector3(0, 7.1f, 10f), new Vector3(4f, 0.5f, 4f), _matGelViolet, ColVioletLt);
|
|
||||||
// Second wall going down (face +Z)
|
|
||||||
CreateZone("E_Wall2b", o + new Vector3(0, 3.5f, 12f), new Vector3(4f, 7f, 0.5f), _matGelViolet, ColViolet);
|
|
||||||
// Landing after L traverse
|
|
||||||
CreateZone("E_Land2", o + new Vector3(0, 0.2f, 14f), new Vector3(5f, 0.4f, 4f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// === Vertical tunnel: two sticky walls face-to-face, zigzag up ===
|
|
||||||
// Left wall
|
|
||||||
CreateZone("E_Tun_L", o + new Vector3(-3.5f, 6f, 18f), new Vector3(0.5f, 12f, 4f), _matGelViolet, ColViolet);
|
|
||||||
// Right wall
|
|
||||||
CreateZone("E_Tun_R", o + new Vector3(3.5f, 6f, 18f), new Vector3(0.5f, 12f, 4f), _matGelViolet, ColVioletLt);
|
|
||||||
// Small ledges alternating sides to help zigzag
|
|
||||||
for (int i = 0; i < 4; i++)
|
|
||||||
{
|
|
||||||
float side = (i % 2 == 0) ? -2.5f : 2.5f;
|
|
||||||
float h = 2f + i * 2.8f;
|
|
||||||
CreateZone("E_Tun_Ledge" + i, o + new Vector3(side, h, 18f),
|
|
||||||
new Vector3(2f, 0.3f, 3f), _matNormal, ColNormalLt);
|
|
||||||
}
|
|
||||||
// Top of tunnel
|
|
||||||
CreateZone("E_TunTop", o + new Vector3(0, 12.2f, 18f), new Vector3(5f, 0.4f, 5f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
Debug.Log("[ArenaZoneBuilder] Zone E (Sticky Walls) built.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// ZONE F — COMBO CHALLENGE (North)
|
|
||||||
// Origin: (0, 0, 30)
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private void BuildZoneF_Combo()
|
|
||||||
{
|
|
||||||
Vector3 o = new Vector3(0f, 0f, 30f);
|
|
||||||
|
|
||||||
// Sign
|
|
||||||
CreateSign("Sign_F", o + new Vector3(-5f, 1.5f, -4f), ColSignGold);
|
|
||||||
|
|
||||||
// Entry
|
|
||||||
CreateZone("F_Entry", o + new Vector3(0, 0.15f, 0), new Vector3(5f, 0.3f, 5f), _matNormal, ColNormalLt);
|
|
||||||
|
|
||||||
// Step 1: Speed strip launch
|
|
||||||
CreateZone("F_Speed", o + new Vector3(0, 0.12f, 4f), new Vector3(3f, 0.15f, 6f), _matGelOrange, ColOrange);
|
|
||||||
CreateZone("F_GuidL", o + new Vector3(-2f, 0.5f, 4f), new Vector3(0.3f, 0.7f, 6f), _matNormal, ColGuide);
|
|
||||||
CreateZone("F_GuidR", o + new Vector3(2f, 0.5f, 4f), new Vector3(0.3f, 0.7f, 6f), _matNormal, ColGuide);
|
|
||||||
|
|
||||||
// Step 2: Ramp -> bounce pad
|
|
||||||
CreateRamp("F_Ramp", o + new Vector3(0, 0.6f, 8.5f), new Vector3(3f, 0.25f, 3f),
|
|
||||||
22f, Vector3.right, _matGelOrange, ColOrangeLt);
|
|
||||||
CreateZone("F_BncPad", o + new Vector3(0, 0.2f, 13f), new Vector3(4f, 0.25f, 4f), _matGelBleu, ColBleu);
|
|
||||||
|
|
||||||
// Step 3: High platform with sticky wall leading higher
|
|
||||||
CreateZone("F_MidPlat", o + new Vector3(0, 4.5f, 17f), new Vector3(5f, 0.4f, 4f), _matNormal, ColNormalLt);
|
|
||||||
CreateZone("F_StickyWall", o + new Vector3(-3f, 7f, 17f), new Vector3(0.5f, 5f, 4f), _matGelViolet, ColViolet);
|
|
||||||
|
|
||||||
// Step 4: Moving platform to final
|
|
||||||
Vector3 mpA = o + new Vector3(0, 9.5f, 17f);
|
|
||||||
Vector3 mpB = o + new Vector3(0, 9.5f, 23f);
|
|
||||||
var mp = CreateZone("F_MovPlat", mpA, new Vector3(4f, 0.5f, 4f), _matNormal, ColDark);
|
|
||||||
AddMovingPlatform(mp, mpA, mpB, 0.25f);
|
|
||||||
|
|
||||||
// Step 5: Gold finish platform
|
|
||||||
CreateZone("F_Finish", o + new Vector3(0, 10f, 27f), new Vector3(6f, 0.5f, 5f), _matNormal, ColGold);
|
|
||||||
CreateZone("F_FinBnc", o + new Vector3(0, 10.3f, 27f), new Vector3(3f, 0.2f, 3f), _matGelBleu, ColBleuLt);
|
|
||||||
|
|
||||||
Debug.Log("[ArenaZoneBuilder] Zone F (Combo) built.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// ZONE G — CENTRAL TOWER (Center)
|
|
||||||
// Origin: (0, 0, 5)
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private void BuildZoneG_CentralTower()
|
|
||||||
{
|
|
||||||
Vector3 o = new Vector3(0f, 0f, 5f);
|
|
||||||
|
|
||||||
// Base (accessible from ground)
|
|
||||||
CreateZone("G_Base", o + new Vector3(0, 0.25f, 0), new Vector3(10f, 0.5f, 10f), _matNormal, ColNormal);
|
|
||||||
|
|
||||||
// Access ramp from south
|
|
||||||
CreateRamp("G_Ramp", o + new Vector3(0, 0.8f, -6f), new Vector3(4f, 0.3f, 5f),
|
|
||||||
-10f, Vector3.right, _matNormal, ColNormal);
|
|
||||||
|
|
||||||
// Level 1: Bounce pad -> L2
|
|
||||||
CreateZone("G_L1_Bnc", o + new Vector3(0, 0.5f, 0), new Vector3(3.5f, 0.25f, 3.5f), _matGelBleu, ColBleu);
|
|
||||||
|
|
||||||
// Level 2 (3.5m): platform + speed strip
|
|
||||||
CreateZone("G_L2", o + new Vector3(0, 3.5f, 0), new Vector3(8f, 0.4f, 8f), _matNormal, ColNormalLt);
|
|
||||||
CreateZone("G_L2_Spd", o + new Vector3(0, 3.7f, 0), new Vector3(6f, 0.15f, 2f), _matGelOrange, ColOrange);
|
|
||||||
CreateRamp("G_L2_Ramp", o + new Vector3(3f, 4.5f, 3f), new Vector3(3f, 0.25f, 3f),
|
|
||||||
25f, Vector3.right, _matGelOrange, ColOrangeLt);
|
|
||||||
|
|
||||||
// Level 3 (7m): platform + moving platform to L4
|
|
||||||
CreateZone("G_L3", o + new Vector3(0, 7f, 0), new Vector3(7f, 0.4f, 7f), _matNormal, ColNormalLt);
|
|
||||||
CreateZone("G_L3_Bnc", o + new Vector3(2f, 7.25f, 2f), new Vector3(3f, 0.2f, 3f), _matGelBleu, ColBleuLt);
|
|
||||||
|
|
||||||
// Moving platform L3->L4
|
|
||||||
Vector3 mp3a = o + new Vector3(5f, 8f, 0);
|
|
||||||
Vector3 mp3b = o + new Vector3(0, 8f, 5f);
|
|
||||||
var m3 = CreateZone("G_MP3", mp3a, new Vector3(3.5f, 0.5f, 3.5f), _matNormal, ColDark);
|
|
||||||
AddMovingPlatform(m3, mp3a, mp3b, 0.2f);
|
|
||||||
|
|
||||||
// Level 4 (10.5m): platform + sticky wall to L5
|
|
||||||
CreateZone("G_L4", o + new Vector3(0, 10.5f, 0), new Vector3(7f, 0.4f, 7f), _matNormal, ColNormalLt);
|
|
||||||
CreateZone("G_L4_Sticky", o + new Vector3(-3.8f, 13f, 0), new Vector3(0.5f, 5f, 5f), _matGelViolet, ColViolet);
|
|
||||||
|
|
||||||
// Level 5 — SUMMIT (15.5m): gold platform
|
|
||||||
CreateZone("G_L5", o + new Vector3(0, 15.5f, 0), new Vector3(8f, 0.5f, 8f), _matNormal, ColGold);
|
|
||||||
CreateZone("G_L5_Bnc", o + new Vector3(0, 15.85f, 0), new Vector3(4f, 0.25f, 4f), _matGelBleu, ColBleuLt);
|
|
||||||
|
|
||||||
// Orbital moving platform for alternative L3->L4 access
|
|
||||||
Vector3 orb_a = o + new Vector3(-6f, 9f, 0);
|
|
||||||
Vector3 orb_b = o + new Vector3(0, 9f, -6f);
|
|
||||||
var orb = CreateZone("G_Orb", orb_a, new Vector3(3.5f, 0.5f, 3.5f), _matNormal, ColDark);
|
|
||||||
AddMovingPlatform(orb, orb_a, orb_b, 0.18f);
|
|
||||||
|
|
||||||
Debug.Log("[ArenaZoneBuilder] Zone G (Central Tower) built.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// SIGN HELPER (zone entrance markers)
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private void CreateSign(string name, Vector3 position, Color color)
|
|
||||||
{
|
|
||||||
// Tall thin panel as zone marker
|
|
||||||
CreateZone(name, position, new Vector3(0.3f, 2.5f, 0.3f), _matNormal, color);
|
|
||||||
// Colored cap on top
|
|
||||||
CreateZone(name + "_Cap", position + new Vector3(0, 1.45f, 0), new Vector3(0.8f, 0.4f, 0.8f), _matNormal, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// MATERIAL LOADING
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private void LoadMaterials()
|
|
||||||
{
|
|
||||||
_matGelBleu = Resources.Load<PhysicsMaterial>("GelBleu");
|
|
||||||
_matGelOrange = Resources.Load<PhysicsMaterial>("GelOrange");
|
|
||||||
_matGelViolet = Resources.Load<PhysicsMaterial>("GelViolet");
|
|
||||||
_matBouncy = Resources.Load<PhysicsMaterial>("Bouncy");
|
|
||||||
_matNormal = Resources.Load<PhysicsMaterial>("Normal");
|
|
||||||
|
|
||||||
if (_matGelBleu == null) Debug.LogWarning("[ArenaZoneBuilder] GelBleu not found in Resources!");
|
|
||||||
if (_matGelOrange == null) Debug.LogWarning("[ArenaZoneBuilder] GelOrange not found in Resources!");
|
|
||||||
if (_matGelViolet == null) Debug.LogWarning("[ArenaZoneBuilder] GelViolet not found in Resources!");
|
|
||||||
if (_matBouncy == null) Debug.LogWarning("[ArenaZoneBuilder] Bouncy not found in Resources!");
|
|
||||||
if (_matNormal == null) Debug.LogWarning("[ArenaZoneBuilder] Normal not found in Resources!");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FindBaseMaterial()
|
|
||||||
{
|
|
||||||
var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
|
|
||||||
_baseMat = new Material(shader);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// ZONE HELPERS
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private GameObject CreateZone(string name, Vector3 position, Vector3 size,
|
|
||||||
PhysicsMaterial physMat, Color color)
|
|
||||||
{
|
|
||||||
var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|
||||||
go.name = name;
|
|
||||||
go.transform.position = position;
|
|
||||||
go.transform.localScale = size;
|
|
||||||
go.isStatic = true;
|
|
||||||
|
|
||||||
var col = go.GetComponent<Collider>();
|
|
||||||
if (col != null && physMat != null) col.material = physMat;
|
|
||||||
|
|
||||||
var rend = go.GetComponent<Renderer>();
|
|
||||||
if (rend != null)
|
|
||||||
{
|
|
||||||
var mat = new Material(_baseMat);
|
|
||||||
SetMatColor(mat, color);
|
|
||||||
if (color.a < 1f) SetMatTransparent(mat, color);
|
|
||||||
rend.material = mat;
|
|
||||||
}
|
|
||||||
return go;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GameObject CreateRamp(string name, Vector3 position, Vector3 size,
|
|
||||||
float angle, Vector3 axis, PhysicsMaterial physMat, Color color)
|
|
||||||
{
|
|
||||||
var go = CreateZone(name, position, size, physMat, color);
|
|
||||||
go.transform.rotation = Quaternion.AngleAxis(angle, axis);
|
|
||||||
return go;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddMovingPlatform(GameObject go, Vector3 posA, Vector3 posB, float speed)
|
|
||||||
{
|
|
||||||
go.isStatic = false;
|
|
||||||
_movingPlatforms.Add(new MovingPlatform { go = go, posA = posA, posB = posB, speed = speed });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// MATERIAL HELPERS
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private static void SetMatColor(Material mat, Color color)
|
|
||||||
{
|
|
||||||
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
|
|
||||||
if (mat.HasProperty("_Color")) mat.color = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SetMatTransparent(Material mat, Color color)
|
|
||||||
{
|
|
||||||
if (mat.HasProperty("_Surface"))
|
|
||||||
{
|
|
||||||
mat.SetFloat("_Surface", 1);
|
|
||||||
mat.SetFloat("_Blend", 0);
|
|
||||||
}
|
|
||||||
mat.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
|
|
||||||
mat.DisableKeyword("_SURFACE_TYPE_OPAQUE");
|
|
||||||
mat.EnableKeyword("_ALPHAPREMULTIPLY_ON");
|
|
||||||
mat.renderQueue = 3000;
|
|
||||||
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
|
|
||||||
if (mat.HasProperty("_Color")) mat.color = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// MOVING PLATFORM DATA
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
private class MovingPlatform
|
|
||||||
{
|
|
||||||
public GameObject go;
|
|
||||||
public Vector3 posA;
|
|
||||||
public Vector3 posB;
|
|
||||||
public float speed;
|
|
||||||
public float t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 31c6f8ef706b51448b461b2b027e2ea8
|
|
||||||
@@ -29,7 +29,9 @@ public class CameraOrbitKeyboard : MonoBehaviour
|
|||||||
{
|
{
|
||||||
// On gère la souris nous-mêmes
|
// On gère la souris nous-mêmes
|
||||||
if (_axisController != null) _axisController.enabled = false;
|
if (_axisController != null) _axisController.enabled = false;
|
||||||
LockCursor();
|
// Only lock cursor if no UI panel is open
|
||||||
|
if (!ChatUI.IsVisible && !KeyBindingUI.IsVisible)
|
||||||
|
LockCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnDisable()
|
void OnDisable()
|
||||||
@@ -55,16 +57,16 @@ public class CameraOrbitKeyboard : MonoBehaviour
|
|||||||
|
|
||||||
var mouse = Mouse.current;
|
var mouse = Mouse.current;
|
||||||
|
|
||||||
// Clic droit = toggle lock
|
// Right-click unlocks, left-click re-locks (consistent with PlayerController)
|
||||||
if (mouse != null && mouse.rightButton.wasPressedThisFrame)
|
if (!ChatUI.IsVisible && !KeyBindingUI.IsVisible && mouse != null)
|
||||||
{
|
{
|
||||||
if (Cursor.lockState == CursorLockMode.Locked)
|
if (Cursor.lockState == CursorLockMode.Locked && mouse.rightButton.wasPressedThisFrame)
|
||||||
UnlockCursor();
|
UnlockCursor();
|
||||||
else
|
else if (Cursor.lockState != CursorLockMode.Locked && mouse.leftButton.wasPressedThisFrame)
|
||||||
LockCursor();
|
LockCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (KeyBindingUI.IsVisible) return;
|
if (KeyBindingUI.IsVisible || ChatUI.IsVisible) return;
|
||||||
|
|
||||||
// Souris — seulement quand locked (delta infini, sans accrochage au bord)
|
// Souris — seulement quand locked (delta infini, sans accrochage au bord)
|
||||||
if (Cursor.lockState == CursorLockMode.Locked && mouse != null)
|
if (Cursor.lockState == CursorLockMode.Locked && mouse != null)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ public class GameManager : MonoBehaviour
|
|||||||
public GameObject playerRoot;
|
public GameObject playerRoot;
|
||||||
public SpectatorCamera spectatorCamera;
|
public SpectatorCamera spectatorCamera;
|
||||||
public GameHUD gameHUD;
|
public GameHUD gameHUD;
|
||||||
public EliminationOverlay eliminationOverlay;
|
|
||||||
|
|
||||||
public GamePhase CurrentPhase { get; private set; } = GamePhase.Lobby;
|
public GamePhase CurrentPhase { get; private set; } = GamePhase.Lobby;
|
||||||
public bool IsLocalEliminated { get; private set; } = false;
|
public bool IsLocalEliminated { get; private set; } = false;
|
||||||
@@ -27,32 +26,32 @@ public class GameManager : MonoBehaviour
|
|||||||
DontDestroyOnLoad(gameObject);
|
DontDestroyOnLoad(gameObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnEnable()
|
void Start()
|
||||||
{
|
{
|
||||||
var nm = NetworkManager.Instance;
|
var nm = NetworkManager.Instance;
|
||||||
if (nm == null) return;
|
if (nm == null) return;
|
||||||
nm.OnPhaseChanged += HandlePhaseChanged;
|
nm.OnPhaseChanged += HandlePhaseChanged;
|
||||||
nm.OnCountdownChanged += HandleCountdownChanged;
|
nm.OnCountdownChanged += HandleCountdownChanged;
|
||||||
nm.OnEliminated += HandleEliminated;
|
nm.OnEliminated += HandleEliminated;
|
||||||
nm.OnQualified += HandleQualified;
|
nm.OnQualified += HandleQualified;
|
||||||
nm.OnRoundStart += HandleRoundStart;
|
nm.OnRoundStart += HandleRoundStart;
|
||||||
nm.OnRoundEnd += HandleRoundEnd;
|
nm.OnRoundEnd += HandleRoundEnd;
|
||||||
nm.OnGameEnd += HandleGameEnd;
|
nm.OnGameEnd += HandleGameEnd;
|
||||||
nm.OnDisconnected += HandleDisconnected;
|
nm.OnDisconnected += HandleDisconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnDisable()
|
void OnDestroy()
|
||||||
{
|
{
|
||||||
var nm = NetworkManager.Instance;
|
var nm = NetworkManager.Instance;
|
||||||
if (nm == null) return;
|
if (nm == null) return;
|
||||||
nm.OnPhaseChanged -= HandlePhaseChanged;
|
nm.OnPhaseChanged -= HandlePhaseChanged;
|
||||||
nm.OnCountdownChanged -= HandleCountdownChanged;
|
nm.OnCountdownChanged -= HandleCountdownChanged;
|
||||||
nm.OnEliminated -= HandleEliminated;
|
nm.OnEliminated -= HandleEliminated;
|
||||||
nm.OnQualified -= HandleQualified;
|
nm.OnQualified -= HandleQualified;
|
||||||
nm.OnRoundStart -= HandleRoundStart;
|
nm.OnRoundStart -= HandleRoundStart;
|
||||||
nm.OnRoundEnd -= HandleRoundEnd;
|
nm.OnRoundEnd -= HandleRoundEnd;
|
||||||
nm.OnGameEnd -= HandleGameEnd;
|
nm.OnGameEnd -= HandleGameEnd;
|
||||||
nm.OnDisconnected -= HandleDisconnected;
|
nm.OnDisconnected -= HandleDisconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Event Handlers ───────────────────────────────────────────────────
|
// ─── Event Handlers ───────────────────────────────────────────────────
|
||||||
@@ -93,7 +92,6 @@ public class GameManager : MonoBehaviour
|
|||||||
{
|
{
|
||||||
IsLocalEliminated = true;
|
IsLocalEliminated = true;
|
||||||
TransitionTo(GamePhase.Eliminated);
|
TransitionTo(GamePhase.Eliminated);
|
||||||
eliminationOverlay?.ShowEliminated();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,15 +100,15 @@ public class GameManager : MonoBehaviour
|
|||||||
if (sessionId == NetworkManager.Instance?.LocalSessionId)
|
if (sessionId == NetworkManager.Instance?.LocalSessionId)
|
||||||
{
|
{
|
||||||
TransitionTo(GamePhase.Qualified);
|
TransitionTo(GamePhase.Qualified);
|
||||||
eliminationOverlay?.ShowQualified();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleRoundStart(int round, string mode)
|
void HandleRoundStart(int round, string mode, int totalRounds)
|
||||||
{
|
{
|
||||||
CurrentRound = round;
|
CurrentRound = round;
|
||||||
CurrentMode = mode;
|
CurrentMode = mode;
|
||||||
gameHUD?.SetRoundInfo(round, mode);
|
gameHUD?.SetRoundInfo(round, mode);
|
||||||
|
gameHUD?.SetTotalRounds(totalRounds);
|
||||||
IsLocalEliminated = false;
|
IsLocalEliminated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +119,6 @@ public class GameManager : MonoBehaviour
|
|||||||
|
|
||||||
void HandleGameEnd(string winner)
|
void HandleGameEnd(string winner)
|
||||||
{
|
{
|
||||||
eliminationOverlay?.ShowGameEnd(winner);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleDisconnected()
|
void HandleDisconnected()
|
||||||
@@ -140,8 +137,7 @@ public class GameManager : MonoBehaviour
|
|||||||
switch (phase)
|
switch (phase)
|
||||||
{
|
{
|
||||||
case GamePhase.Lobby:
|
case GamePhase.Lobby:
|
||||||
SetPlayerActive(false);
|
SetPlayerActive(NetworkManager.Instance?.IsConnected ?? false);
|
||||||
SetSpectatorActive(false);
|
|
||||||
gameHUD?.SetPhase("lobby");
|
gameHUD?.SetPhase("lobby");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class KeyBindingUI : MonoBehaviour
|
|||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
if (Keyboard.current != null && Keyboard.current[Key.F2].wasPressedThisFrame)
|
if (Keyboard.current != null && Keyboard.current[Key.Tab].wasPressedThisFrame)
|
||||||
{
|
{
|
||||||
_visible = !_visible;
|
_visible = !_visible;
|
||||||
IsVisible = _visible;
|
IsVisible = _visible;
|
||||||
@@ -132,7 +132,7 @@ public class KeyBindingUI : MonoBehaviour
|
|||||||
GUILayout.BeginHorizontal();
|
GUILayout.BeginHorizontal();
|
||||||
if (GUILayout.Button("Réinitialiser tout", ImGuiSkin.Button, GUILayout.Height(32)))
|
if (GUILayout.Button("Réinitialiser tout", ImGuiSkin.Button, GUILayout.Height(32)))
|
||||||
ResetAllBindings();
|
ResetAllBindings();
|
||||||
if (GUILayout.Button("Fermer (F2)", ImGuiSkin.Button, GUILayout.Height(32)))
|
if (GUILayout.Button("Fermer (Tab)", ImGuiSkin.Button, GUILayout.Height(32)))
|
||||||
{
|
{
|
||||||
_visible = false;
|
_visible = false;
|
||||||
CancelRebind();
|
CancelRebind();
|
||||||
@@ -146,8 +146,7 @@ public class KeyBindingUI : MonoBehaviour
|
|||||||
GUILayout.Label("Appuyez sur une touche pour assigner...", ImGuiSkin.Hint);
|
GUILayout.Label("Appuyez sur une touche pour assigner...", ImGuiSkin.Hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// F2 hint
|
GUILayout.Label("Tab — Ouvrir / Fermer ce menu", ImGuiSkin.Footer);
|
||||||
GUILayout.Label("F2 — Ouvrir / Fermer ce menu", ImGuiSkin.Footer);
|
|
||||||
|
|
||||||
ImGuiSkin.EndWindow();
|
ImGuiSkin.EndWindow();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ public class DebugNetworkUI : MonoBehaviour
|
|||||||
_fpsCount = 0;
|
_fpsCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle detailed panel with F1
|
// Toggle detailed panel with ` (backtick)
|
||||||
if (Keyboard.current != null && Keyboard.current[Key.F1].wasPressedThisFrame)
|
if (Keyboard.current != null && Keyboard.current[Key.Backquote].wasPressedThisFrame)
|
||||||
_detailsVisible = !_detailsVisible;
|
_detailsVisible = !_detailsVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ public class DebugNetworkUI : MonoBehaviour
|
|||||||
DrawDetailPanel(nm);
|
DrawDetailPanel(nm);
|
||||||
|
|
||||||
// Hint
|
// Hint
|
||||||
GUI.Label(new Rect(10, Screen.height - 25, 300, 20), "F1 — Debug details", ImGuiSkin.Footer);
|
GUI.Label(new Rect(10, Screen.height - 25, 300, 20), "` — Debug details", ImGuiSkin.Footer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────── HUD Strip (always visible) ─────────
|
// ───────── HUD Strip (always visible) ─────────
|
||||||
|
|||||||
@@ -1,106 +1,126 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lobby UI displayed at scene start. Player enters a name, picks a color,
|
/// Lobby UI: character setup + room list side by side.
|
||||||
/// and clicks "Rejoindre" to connect to the arena.
|
/// - T to open/close chat, Tab for keybinds (handled elsewhere)
|
||||||
/// Manages the full pre-game → in-game transition:
|
/// - Lists available rooms, lets the player create or join one
|
||||||
/// - Hides the Player hierarchy until connected
|
|
||||||
/// - Activates a spectator camera while in lobby
|
|
||||||
/// - Teleports the player ball to the server spawn position on join
|
|
||||||
/// Uses Dear ImGui–style skin via ImGuiSkin.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LobbyUI : MonoBehaviour
|
public class LobbyUI : MonoBehaviour
|
||||||
{
|
{
|
||||||
[Header("Scene References")]
|
[Header("Scene References")]
|
||||||
[Tooltip("The root 'Player' GameObject (contains PlayerSphere + cameras). Will be deactivated until connected.")]
|
|
||||||
public GameObject playerRoot;
|
public GameObject playerRoot;
|
||||||
|
|
||||||
[Tooltip("The spectator camera GameObject (SpectatorCamera component).")]
|
|
||||||
public SpectatorCamera spectatorCamera;
|
public SpectatorCamera spectatorCamera;
|
||||||
|
|
||||||
// Preset colors for selection
|
private static readonly Color[] PresetColors =
|
||||||
private static readonly Color[] PresetColors = new Color[]
|
|
||||||
{
|
{
|
||||||
new Color(1f, 0.35f, 0.2f), // Orange-red
|
new Color(1f, 0.35f, 0.2f),
|
||||||
new Color(0.2f, 0.6f, 1f), // Blue
|
new Color(0.2f, 0.6f, 1f),
|
||||||
new Color(0.3f, 1f, 0.4f), // Green
|
new Color(0.3f, 1f, 0.4f),
|
||||||
new Color(1f, 0.85f, 0.1f), // Yellow
|
new Color(1f, 0.85f, 0.1f),
|
||||||
new Color(0.8f, 0.3f, 1f), // Purple
|
new Color(0.8f, 0.3f, 1f),
|
||||||
new Color(1f, 0.5f, 0.7f), // Pink
|
new Color(1f, 0.5f, 0.7f),
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly string[] ColorNames = new string[]
|
|
||||||
{
|
|
||||||
"Rouge", "Bleu", "Vert", "Jaune", "Violet", "Rose"
|
|
||||||
};
|
};
|
||||||
|
private static readonly string[] ColorNames = { "Rouge", "Bleu", "Vert", "Jaune", "Violet", "Rose" };
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
private bool _lobbyActive = true;
|
private bool _lobbyActive = true;
|
||||||
private string _playerName = "";
|
private string _playerName = "";
|
||||||
private int _selectedColorIndex = 0;
|
private int _selectedColorIndex = 0;
|
||||||
private string _statusMessage = "";
|
private string _statusMessage = "";
|
||||||
private bool _isConnecting = false;
|
private bool _isConnecting = false;
|
||||||
private bool _isReady = false;
|
private bool _isReady = false;
|
||||||
|
|
||||||
// Cached color preview texture (avoid per-frame leak)
|
// Room list
|
||||||
|
private NetworkManager.RoomInfo[] _rooms = new NetworkManager.RoomInfo[0];
|
||||||
|
private bool _roomsFetching = false;
|
||||||
|
private float _refreshTimer = 0f;
|
||||||
|
private const float REFRESH_INTERVAL = 4f;
|
||||||
|
private Vector2 _roomsScroll;
|
||||||
|
|
||||||
|
// Color preview texture
|
||||||
private Texture2D _colorPreviewTex;
|
private Texture2D _colorPreviewTex;
|
||||||
private int _lastPreviewColorIndex = -1;
|
private int _lastPreviewColorIndex = -1;
|
||||||
|
|
||||||
void Start()
|
void Start()
|
||||||
{
|
{
|
||||||
// Generate a default name
|
_playerName = PlayerPrefs.GetString("rolld_player_name", "Joueur" + Random.Range(100, 999));
|
||||||
_playerName = "Joueur" + Random.Range(100, 999);
|
|
||||||
|
|
||||||
// --- Hide the player hierarchy until connected ---
|
|
||||||
if (playerRoot != null)
|
if (playerRoot != null)
|
||||||
playerRoot.SetActive(false);
|
playerRoot.SetActive(false);
|
||||||
|
|
||||||
// --- Activate spectator camera ---
|
|
||||||
if (spectatorCamera != null)
|
if (spectatorCamera != null)
|
||||||
{
|
{
|
||||||
// Wire the gameplay camera reference so spectator knows what to re-enable
|
|
||||||
var gameplayCam = playerRoot?.GetComponentInChildren<Camera>(true);
|
var gameplayCam = playerRoot?.GetComponentInChildren<Camera>(true);
|
||||||
if (gameplayCam != null)
|
if (gameplayCam != null)
|
||||||
spectatorCamera.gameplayCamera = gameplayCam;
|
spectatorCamera.gameplayCamera = gameplayCam;
|
||||||
|
|
||||||
spectatorCamera.Activate();
|
spectatorCamera.Activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to network events
|
var nm = NetworkManager.Instance;
|
||||||
if (NetworkManager.Instance != null)
|
if (nm != null)
|
||||||
{
|
{
|
||||||
NetworkManager.Instance.OnConnected += OnConnected;
|
nm.OnConnected += OnConnected;
|
||||||
NetworkManager.Instance.OnDisconnected += OnDisconnected;
|
nm.OnDisconnected += OnDisconnected;
|
||||||
|
nm.OnRoomsRefreshed += OnRoomsRefreshed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RefreshRooms();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnDestroy()
|
void OnDestroy()
|
||||||
{
|
{
|
||||||
if (NetworkManager.Instance != null)
|
var nm = NetworkManager.Instance;
|
||||||
|
if (nm != null)
|
||||||
{
|
{
|
||||||
NetworkManager.Instance.OnConnected -= OnConnected;
|
nm.OnConnected -= OnConnected;
|
||||||
NetworkManager.Instance.OnDisconnected -= OnDisconnected;
|
nm.OnDisconnected -= OnDisconnected;
|
||||||
|
nm.OnRoomsRefreshed -= OnRoomsRefreshed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (!_lobbyActive || _isConnecting) return;
|
||||||
|
_refreshTimer += Time.deltaTime;
|
||||||
|
if (_refreshTimer >= REFRESH_INTERVAL)
|
||||||
|
{
|
||||||
|
_refreshTimer = 0f;
|
||||||
|
RefreshRooms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshRooms()
|
||||||
|
{
|
||||||
|
if (_roomsFetching) return;
|
||||||
|
_roomsFetching = true;
|
||||||
|
NetworkManager.Instance?.FetchRooms();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRoomsRefreshed(NetworkManager.RoomInfo[] rooms)
|
||||||
|
{
|
||||||
|
_rooms = rooms;
|
||||||
|
_roomsFetching = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Network callbacks ────────────────────────────────────────────────
|
||||||
|
|
||||||
private void OnConnected()
|
private void OnConnected()
|
||||||
{
|
{
|
||||||
_lobbyActive = false;
|
_lobbyActive = false;
|
||||||
_isConnecting = false;
|
_isConnecting = false;
|
||||||
_statusMessage = "";
|
_statusMessage = "";
|
||||||
|
CancelInvoke(nameof(ConnectionTimeout));
|
||||||
|
|
||||||
// --- Activate the player hierarchy ---
|
|
||||||
if (playerRoot != null)
|
if (playerRoot != null)
|
||||||
playerRoot.SetActive(true);
|
playerRoot.SetActive(true);
|
||||||
|
|
||||||
// Teleport player ball to the server-assigned spawn position
|
|
||||||
var nm = NetworkManager.Instance;
|
var nm = NetworkManager.Instance;
|
||||||
if (nm != null && playerRoot != null)
|
if (nm != null && playerRoot != null)
|
||||||
{
|
{
|
||||||
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
||||||
if (pc != null)
|
if (pc != null)
|
||||||
{
|
{
|
||||||
// Get spawn pos from the local player's state in the room
|
|
||||||
var localState = nm.GetLocalPlayerState();
|
var localState = nm.GetLocalPlayerState();
|
||||||
if (localState != null)
|
if (localState != null)
|
||||||
{
|
{
|
||||||
@@ -108,41 +128,36 @@ public class LobbyUI : MonoBehaviour
|
|||||||
var rb = pc.GetComponent<Rigidbody>();
|
var rb = pc.GetComponent<Rigidbody>();
|
||||||
if (rb != null)
|
if (rb != null)
|
||||||
{
|
{
|
||||||
rb.linearVelocity = Vector3.zero;
|
rb.linearVelocity = Vector3.zero;
|
||||||
rb.angularVelocity = Vector3.zero;
|
rb.angularVelocity = Vector3.zero;
|
||||||
rb.position = spawnPos;
|
rb.position = spawnPos;
|
||||||
}
|
}
|
||||||
pc.transform.position = spawnPos;
|
pc.transform.position = spawnPos;
|
||||||
Debug.Log($"[Lobby] Player teleported to spawn: {spawnPos}");
|
pc.SetSpawnPosition(spawnPos);
|
||||||
}
|
}
|
||||||
pc.enabled = true;
|
pc.enabled = true;
|
||||||
|
|
||||||
// Setup local player visuals: 50% color tint + floating name label
|
|
||||||
pc.SetupLocalPlayer(nm.LocalPlayerName, nm.LocalPlayerColor);
|
pc.SetupLocalPlayer(nm.LocalPlayerName, nm.LocalPlayerColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Switch from spectator to gameplay camera ---
|
|
||||||
if (spectatorCamera != null)
|
if (spectatorCamera != null)
|
||||||
spectatorCamera.Deactivate();
|
spectatorCamera.Deactivate();
|
||||||
|
|
||||||
// Unlock cursor for gameplay
|
|
||||||
Cursor.lockState = CursorLockMode.Locked;
|
Cursor.lockState = CursorLockMode.Locked;
|
||||||
Cursor.visible = false;
|
Cursor.visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisconnected()
|
private void OnDisconnected()
|
||||||
{
|
{
|
||||||
_lobbyActive = true;
|
_lobbyActive = true;
|
||||||
_isConnecting = false;
|
_isConnecting = false;
|
||||||
_isReady = false;
|
_isReady = false;
|
||||||
_statusMessage = "Déconnecté du serveur";
|
_statusMessage = "Déconnecté du serveur";
|
||||||
|
_refreshTimer = REFRESH_INTERVAL; // force immediate refresh
|
||||||
|
|
||||||
// Show cursor for lobby
|
|
||||||
Cursor.lockState = CursorLockMode.None;
|
Cursor.lockState = CursorLockMode.None;
|
||||||
Cursor.visible = true;
|
Cursor.visible = true;
|
||||||
|
|
||||||
// --- Deactivate the player hierarchy ---
|
|
||||||
if (playerRoot != null)
|
if (playerRoot != null)
|
||||||
{
|
{
|
||||||
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
||||||
@@ -150,188 +165,267 @@ public class LobbyUI : MonoBehaviour
|
|||||||
playerRoot.SetActive(false);
|
playerRoot.SetActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Re-enable spectator camera ---
|
|
||||||
if (spectatorCamera != null)
|
if (spectatorCamera != null)
|
||||||
spectatorCamera.Activate();
|
spectatorCamera.Activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── OnGUI ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void OnGUI()
|
void OnGUI()
|
||||||
{
|
{
|
||||||
if (!_lobbyActive) return;
|
if (!_lobbyActive) return;
|
||||||
|
|
||||||
ImGuiSkin.EnsureReady();
|
ImGuiSkin.EnsureReady();
|
||||||
|
|
||||||
if (Cursor.lockState != CursorLockMode.None)
|
if (Cursor.lockState != CursorLockMode.None)
|
||||||
{
|
{
|
||||||
Cursor.lockState = CursorLockMode.None;
|
Cursor.lockState = CursorLockMode.None;
|
||||||
Cursor.visible = true;
|
Cursor.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiSkin.DrawOverlay();
|
ImGuiSkin.DrawOverlay();
|
||||||
|
|
||||||
bool isConnected = NetworkManager.Instance != null && NetworkManager.Instance.IsConnected;
|
bool isConnected = NetworkManager.Instance != null && NetworkManager.Instance.IsConnected;
|
||||||
|
|
||||||
if (!isConnected)
|
if (!isConnected)
|
||||||
|
DrawSetupAndRoomList();
|
||||||
|
else
|
||||||
|
DrawWaitingRoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Setup + room list ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void DrawSetupAndRoomList()
|
||||||
|
{
|
||||||
|
const float W = 620f, H = 520f;
|
||||||
|
float x = (Screen.width - W) * 0.5f;
|
||||||
|
float y = (Screen.height - H) * 0.5f;
|
||||||
|
|
||||||
|
ImGuiSkin.BeginWindowAt(x, y, W, H, "ROLL'D");
|
||||||
|
GUILayout.Label("Choisir une salle et configurer son personnage", ImGuiSkin.WindowSubtitle);
|
||||||
|
GUILayout.Space(10);
|
||||||
|
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
|
||||||
|
// ── Left column : character setup ─────────────────────────────
|
||||||
|
GUILayout.BeginVertical(GUILayout.Width(240));
|
||||||
|
ImGuiSkin.DrawSectionHeader("PERSONNAGE");
|
||||||
|
GUILayout.Space(4);
|
||||||
|
_playerName = GUILayout.TextField(_playerName, 16, ImGuiSkin.TextField, GUILayout.Height(30));
|
||||||
|
GUILayout.Space(10);
|
||||||
|
|
||||||
|
ImGuiSkin.DrawSectionHeader("COULEUR");
|
||||||
|
GUILayout.Space(4);
|
||||||
|
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
for (int i = 0; i < PresetColors.Length; i++)
|
||||||
{
|
{
|
||||||
// ── Pre-connect panel ────────────────────────────────────────
|
Color c = PresetColors[i];
|
||||||
float panelWidth = 420;
|
bool selected = _selectedColorIndex == i;
|
||||||
float panelHeight = 440;
|
Color prevBg = GUI.backgroundColor;
|
||||||
ImGuiSkin.BeginWindow(panelWidth, panelHeight, "ROLL'D");
|
GUI.backgroundColor = selected ? c : c * 0.6f;
|
||||||
|
var btnStyle = new GUIStyle(ImGuiSkin.ButtonSmall)
|
||||||
|
{ fontStyle = selected ? FontStyle.Bold : FontStyle.Normal };
|
||||||
|
if (selected) btnStyle.normal.textColor = Color.white;
|
||||||
|
if (GUILayout.Button(selected ? $"▸{ColorNames[i][0]}" : $"{ColorNames[i][0]}",
|
||||||
|
btnStyle, GUILayout.Height(30), GUILayout.Width(34)))
|
||||||
|
_selectedColorIndex = i;
|
||||||
|
GUI.backgroundColor = prevBg;
|
||||||
|
}
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
|
||||||
GUILayout.Label("Rejoindre l'arène multijoueur", ImGuiSkin.WindowSubtitle);
|
GUILayout.Space(4);
|
||||||
GUILayout.Space(16);
|
// Color swatch
|
||||||
|
if (_colorPreviewTex == null || _lastPreviewColorIndex != _selectedColorIndex)
|
||||||
ImGuiSkin.DrawSectionHeader("PSEUDO");
|
{
|
||||||
GUILayout.Space(4);
|
if (_colorPreviewTex == null)
|
||||||
_playerName = GUILayout.TextField(_playerName, 16, ImGuiSkin.TextField, GUILayout.Height(30));
|
|
||||||
GUILayout.Space(12);
|
|
||||||
|
|
||||||
ImGuiSkin.DrawSectionHeader("COULEUR");
|
|
||||||
GUILayout.Space(6);
|
|
||||||
|
|
||||||
GUILayout.BeginHorizontal();
|
|
||||||
for (int i = 0; i < PresetColors.Length; i++)
|
|
||||||
{
|
{
|
||||||
Color c = PresetColors[i];
|
_colorPreviewTex = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||||
bool selected = _selectedColorIndex == i;
|
_colorPreviewTex.hideFlags = HideFlags.HideAndDontSave;
|
||||||
Color prevBg = GUI.backgroundColor;
|
|
||||||
GUI.backgroundColor = selected ? c : c * 0.7f;
|
|
||||||
GUIStyle btnStyle = new GUIStyle(ImGuiSkin.ButtonSmall)
|
|
||||||
{
|
|
||||||
fontStyle = selected ? FontStyle.Bold : FontStyle.Normal,
|
|
||||||
};
|
|
||||||
if (selected) btnStyle.normal.textColor = Color.white;
|
|
||||||
string label = selected ? $"▸ {ColorNames[i]}" : ColorNames[i];
|
|
||||||
if (GUILayout.Button(label, btnStyle, GUILayout.Height(32), GUILayout.Width(60)))
|
|
||||||
_selectedColorIndex = i;
|
|
||||||
GUI.backgroundColor = prevBg;
|
|
||||||
}
|
}
|
||||||
GUILayout.EndHorizontal();
|
_colorPreviewTex.SetPixel(0, 0, PresetColors[_selectedColorIndex]);
|
||||||
GUILayout.Space(4);
|
_colorPreviewTex.Apply();
|
||||||
|
_lastPreviewColorIndex = _selectedColorIndex;
|
||||||
|
}
|
||||||
|
var swatchStyle = new GUIStyle(ImGuiSkin.LabelDim) { alignment = TextAnchor.MiddleLeft, fontSize = 11 };
|
||||||
|
GUILayout.Label($"▌ {ColorNames[_selectedColorIndex]}", swatchStyle);
|
||||||
|
|
||||||
if (_colorPreviewTex == null || _lastPreviewColorIndex != _selectedColorIndex)
|
GUILayout.FlexibleSpace();
|
||||||
{
|
|
||||||
if (_colorPreviewTex == null)
|
// Create room button
|
||||||
{
|
GUI.enabled = !_isConnecting && !string.IsNullOrWhiteSpace(_playerName);
|
||||||
_colorPreviewTex = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
if (GUILayout.Button("+ Créer une salle", ImGuiSkin.Button, GUILayout.Height(36)))
|
||||||
_colorPreviewTex.hideFlags = HideFlags.HideAndDontSave;
|
DoCreate();
|
||||||
}
|
|
||||||
_colorPreviewTex.SetPixel(0, 0, PresetColors[_selectedColorIndex]);
|
GUILayout.Space(4);
|
||||||
_colorPreviewTex.Apply();
|
|
||||||
_lastPreviewColorIndex = _selectedColorIndex;
|
// Join any (join or create fallback)
|
||||||
}
|
if (GUILayout.Button("▶ Rejoindre n'importe", ImGuiSkin.ButtonAccent, GUILayout.Height(36)))
|
||||||
GUILayout.BeginHorizontal();
|
DoJoinAny();
|
||||||
|
|
||||||
|
GUI.enabled = true;
|
||||||
|
GUILayout.EndVertical();
|
||||||
|
|
||||||
|
GUILayout.Space(12);
|
||||||
|
|
||||||
|
// ── Right column : room list ───────────────────────────────────
|
||||||
|
GUILayout.BeginVertical();
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
ImGuiSkin.DrawSectionHeader("SALLES DISPONIBLES");
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
GUI.enabled = !_roomsFetching;
|
||||||
|
if (GUILayout.Button(_roomsFetching ? "…" : "↻", ImGuiSkin.ButtonSmall,
|
||||||
|
GUILayout.Width(28), GUILayout.Height(22)))
|
||||||
|
{
|
||||||
|
_refreshTimer = 0f;
|
||||||
|
RefreshRooms();
|
||||||
|
}
|
||||||
|
GUI.enabled = true;
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
GUILayout.Space(4);
|
||||||
|
|
||||||
|
float listH = H - 160f;
|
||||||
|
_roomsScroll = GUILayout.BeginScrollView(_roomsScroll, ImGuiSkin.ScrollView,
|
||||||
|
GUILayout.Height(listH));
|
||||||
|
|
||||||
|
if (_rooms.Length == 0)
|
||||||
|
{
|
||||||
|
var emptyStyle = new GUIStyle(ImGuiSkin.LabelDim) { alignment = TextAnchor.MiddleCenter };
|
||||||
GUILayout.FlexibleSpace();
|
GUILayout.FlexibleSpace();
|
||||||
GUILayout.Box(_colorPreviewTex, GUIStyle.none, GUILayout.Width(80), GUILayout.Height(16));
|
GUILayout.Label(_roomsFetching ? "Chargement…" : "Aucune salle ouverte.", emptyStyle);
|
||||||
GUILayout.FlexibleSpace();
|
GUILayout.FlexibleSpace();
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
GUILayout.Space(16);
|
|
||||||
|
|
||||||
GUI.enabled = !_isConnecting && !string.IsNullOrWhiteSpace(_playerName);
|
|
||||||
string buttonText = _isConnecting ? "Connexion..." : "▶ Rejoindre l'arène";
|
|
||||||
if (GUILayout.Button(buttonText, ImGuiSkin.ButtonAccent, GUILayout.Height(44)))
|
|
||||||
JoinArena();
|
|
||||||
GUI.enabled = true;
|
|
||||||
GUILayout.Space(8);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(_statusMessage))
|
|
||||||
{
|
|
||||||
bool isError = _statusMessage.Contains("Erreur") || _statusMessage.Contains("Déconnecté");
|
|
||||||
GUIStyle statusStyle = isError ? ImGuiSkin.StatusRed : new GUIStyle(ImGuiSkin.Hint);
|
|
||||||
if (!isError) statusStyle.normal.textColor = ImGuiSkin.ColYellow;
|
|
||||||
GUILayout.Label(_statusMessage, statusStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGuiSkin.EndWindow();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// ── Waiting room panel (connected, waiting for game to start) ──
|
foreach (var room in _rooms)
|
||||||
float panelWidth = 380;
|
|
||||||
float panelHeight = 320;
|
|
||||||
ImGuiSkin.BeginWindow(panelWidth, panelHeight, "SALLE D'ATTENTE");
|
|
||||||
|
|
||||||
GUILayout.Label("En attente des joueurs...", ImGuiSkin.WindowSubtitle);
|
|
||||||
GUILayout.Space(12);
|
|
||||||
|
|
||||||
// Player list
|
|
||||||
ImGuiSkin.DrawSectionHeader("JOUEURS CONNECTÉS");
|
|
||||||
GUILayout.Space(4);
|
|
||||||
var nm = NetworkManager.Instance;
|
|
||||||
if (nm != null && nm.IsConnected)
|
|
||||||
{
|
{
|
||||||
// We can't directly iterate NetworkState.players from here easily,
|
string roomName = room.metadata?.name ?? ("Salle #" + room.roomId.Substring(0, 6));
|
||||||
// so show basic count
|
int clients = room.clients;
|
||||||
var style = new GUIStyle(GUI.skin.label) { fontSize = 13 };
|
int maxCli = room.maxClients;
|
||||||
style.normal.textColor = new Color(0.75f, 0.75f, 0.85f);
|
|
||||||
GUILayout.Label($" {nm.PlayerCount} joueur(s) dans la salle", style);
|
|
||||||
}
|
|
||||||
GUILayout.Space(16);
|
|
||||||
|
|
||||||
// Ready button
|
GUILayout.BeginHorizontal();
|
||||||
if (!_isReady)
|
|
||||||
{
|
|
||||||
if (GUILayout.Button("✔ Je suis prêt !", ImGuiSkin.ButtonAccent, GUILayout.Height(44)))
|
|
||||||
{
|
|
||||||
_isReady = true;
|
|
||||||
NetworkManager.Instance?.SendReady();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var readyStyle = new GUIStyle(GUI.skin.label)
|
|
||||||
{
|
|
||||||
alignment = TextAnchor.MiddleCenter,
|
|
||||||
fontSize = 16,
|
|
||||||
fontStyle = FontStyle.Bold,
|
|
||||||
};
|
|
||||||
readyStyle.normal.textColor = new Color(0.3f, 1f, 0.5f);
|
|
||||||
GUILayout.Label("✔ Prêt ! En attente des autres...", readyStyle, GUILayout.Height(44));
|
|
||||||
}
|
|
||||||
|
|
||||||
GUILayout.Space(8);
|
var nameStyle = new GUIStyle(ImGuiSkin.LabelBold) { fontSize = 12 };
|
||||||
var hintStyle = new GUIStyle(ImGuiSkin.Hint);
|
GUILayout.Label(roomName, nameStyle, GUILayout.Width(140));
|
||||||
hintStyle.normal.textColor = new Color(0.5f, 0.5f, 0.6f);
|
|
||||||
GUILayout.Label("La partie démarre quand tout le monde est prêt\nou automatiquement après 30 secondes.", hintStyle);
|
|
||||||
|
|
||||||
ImGuiSkin.EndWindow();
|
var countStyle = new GUIStyle(ImGuiSkin.LabelDim) { alignment = TextAnchor.MiddleCenter, fontSize = 11 };
|
||||||
|
GUILayout.Label($"{clients} / {maxCli}", countStyle, GUILayout.Width(48));
|
||||||
|
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
|
||||||
|
bool full = clients >= maxCli;
|
||||||
|
GUI.enabled = !_isConnecting && !full && !string.IsNullOrWhiteSpace(_playerName);
|
||||||
|
if (GUILayout.Button(full ? "Pleine" : "▶ Rejoindre",
|
||||||
|
ImGuiSkin.ButtonSmall, GUILayout.Width(90), GUILayout.Height(26)))
|
||||||
|
DoJoinRoom(room.roomId);
|
||||||
|
GUI.enabled = true;
|
||||||
|
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
ImGuiSkin.Separator();
|
||||||
|
GUILayout.Space(2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GUILayout.EndScrollView();
|
||||||
|
GUILayout.EndVertical();
|
||||||
|
|
||||||
|
GUILayout.EndHorizontal(); // end columns
|
||||||
|
|
||||||
|
// ── Status bar ────────────────────────────────────────────────
|
||||||
|
GUILayout.Space(4);
|
||||||
|
if (!string.IsNullOrEmpty(_statusMessage))
|
||||||
|
{
|
||||||
|
bool isError = _statusMessage.Contains("Erreur") || _statusMessage.Contains("Déconnecté");
|
||||||
|
GUILayout.Label(_statusMessage, isError ? ImGuiSkin.StatusRed : ImGuiSkin.Hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiSkin.EndWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void JoinArena()
|
// ─── Waiting room ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void DrawWaitingRoom()
|
||||||
{
|
{
|
||||||
if (NetworkManager.Instance == null)
|
ImGuiSkin.BeginWindow(400f, 300f, "SALLE D'ATTENTE");
|
||||||
|
|
||||||
|
var nm = NetworkManager.Instance;
|
||||||
|
string roomDisplay = nm != null ? ("Salle #" + nm.RoomId.Substring(0, Mathf.Min(6, nm.RoomId.Length))) : "—";
|
||||||
|
GUILayout.Label(roomDisplay, ImGuiSkin.WindowSubtitle);
|
||||||
|
GUILayout.Space(12);
|
||||||
|
|
||||||
|
ImGuiSkin.DrawSectionHeader("JOUEURS CONNECTÉS");
|
||||||
|
GUILayout.Space(4);
|
||||||
|
if (nm != null)
|
||||||
{
|
{
|
||||||
_statusMessage = "Erreur : NetworkManager introuvable";
|
var s = new GUIStyle(GUI.skin.label) { fontSize = 13 };
|
||||||
return;
|
s.normal.textColor = new Color(0.75f, 0.75f, 0.85f);
|
||||||
|
GUILayout.Label($" {nm.PlayerCount} joueur(s) dans la salle", s);
|
||||||
}
|
}
|
||||||
|
GUILayout.Space(16);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_playerName))
|
if (!_isReady)
|
||||||
{
|
{
|
||||||
_statusMessage = "Entrez un pseudo";
|
if (GUILayout.Button("✔ Je suis prêt !", ImGuiSkin.ButtonAccent, GUILayout.Height(44)))
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isConnecting = true;
|
|
||||||
_statusMessage = "Connexion au serveur...";
|
|
||||||
|
|
||||||
Color selectedColor = PresetColors[_selectedColorIndex];
|
|
||||||
NetworkManager.Instance.JoinArena(_playerName.Trim(), selectedColor);
|
|
||||||
|
|
||||||
// Monitor for errors after a delay
|
|
||||||
Invoke(nameof(CheckConnectionTimeout), 10f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CheckConnectionTimeout()
|
|
||||||
{
|
|
||||||
if (_isConnecting && !NetworkManager.Instance.IsConnected)
|
|
||||||
{
|
|
||||||
_isConnecting = false;
|
|
||||||
_statusMessage = "Erreur : Impossible de joindre rolld.io. Réessayez dans quelques instants.";
|
|
||||||
if (!string.IsNullOrEmpty(NetworkManager.Instance.LastError))
|
|
||||||
{
|
{
|
||||||
_statusMessage += $"\n{NetworkManager.Instance.LastError}";
|
_isReady = true;
|
||||||
|
NetworkManager.Instance?.SendReady();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var rs = new GUIStyle(GUI.skin.label)
|
||||||
|
{ alignment = TextAnchor.MiddleCenter, fontSize = 16, fontStyle = FontStyle.Bold };
|
||||||
|
rs.normal.textColor = new Color(0.3f, 1f, 0.5f);
|
||||||
|
GUILayout.Label("✔ Prêt ! En attente des autres…", rs, GUILayout.Height(44));
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.Space(8);
|
||||||
|
GUILayout.Label("La partie démarre quand tout le monde est prêt\nou automatiquement après 30 secondes.", ImGuiSkin.Hint);
|
||||||
|
|
||||||
|
ImGuiSkin.EndWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Actions ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private string ValidateName()
|
||||||
|
{
|
||||||
|
string n = _playerName.Trim();
|
||||||
|
if (string.IsNullOrEmpty(n)) { _statusMessage = "Entre un pseudo d'abord."; return null; }
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DoJoinRoom(string roomId)
|
||||||
|
{
|
||||||
|
string n = ValidateName(); if (n == null) return;
|
||||||
|
_isConnecting = true;
|
||||||
|
_statusMessage = "Connexion à la salle…";
|
||||||
|
NetworkManager.Instance?.JoinByRoomId(roomId, n, PresetColors[_selectedColorIndex]);
|
||||||
|
Invoke(nameof(ConnectionTimeout), 10f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DoCreate()
|
||||||
|
{
|
||||||
|
string n = ValidateName(); if (n == null) return;
|
||||||
|
_isConnecting = true;
|
||||||
|
_statusMessage = "Création d'une salle…";
|
||||||
|
NetworkManager.Instance?.CreateRoom(n, PresetColors[_selectedColorIndex]);
|
||||||
|
Invoke(nameof(ConnectionTimeout), 10f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DoJoinAny()
|
||||||
|
{
|
||||||
|
string n = ValidateName(); if (n == null) return;
|
||||||
|
_isConnecting = true;
|
||||||
|
_statusMessage = "Connexion…";
|
||||||
|
NetworkManager.Instance?.JoinArena(n, PresetColors[_selectedColorIndex]);
|
||||||
|
Invoke(nameof(ConnectionTimeout), 10f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConnectionTimeout()
|
||||||
|
{
|
||||||
|
if (!_isConnecting) return;
|
||||||
|
_isConnecting = false;
|
||||||
|
var nm = NetworkManager.Instance;
|
||||||
|
_statusMessage = "Impossible de se connecter. Réessaie.";
|
||||||
|
if (nm != null && !string.IsNullOrEmpty(nm.LastError))
|
||||||
|
_statusMessage += $"\n{nm.LastError}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.Networking;
|
||||||
using Colyseus;
|
using Colyseus;
|
||||||
using Colyseus.Schema;
|
using Colyseus.Schema;
|
||||||
|
|
||||||
@@ -33,6 +35,24 @@ public class NetworkManager : MonoBehaviour
|
|||||||
public string LocalPlayerName { get; private set; } = "";
|
public string LocalPlayerName { get; private set; } = "";
|
||||||
public Color LocalPlayerColor { get; private set; } = Color.white;
|
public Color LocalPlayerColor { get; private set; } = Color.white;
|
||||||
|
|
||||||
|
// --- Room listing ---
|
||||||
|
[System.Serializable] public class RoomMeta { public string name; }
|
||||||
|
[System.Serializable] public class RoomInfo { public string roomId; public int clients; public int maxClients; public RoomMeta metadata; }
|
||||||
|
[System.Serializable] private class RoomListWrapper { public List<RoomInfo> items; }
|
||||||
|
|
||||||
|
public event Action<RoomInfo[]> OnRoomsRefreshed;
|
||||||
|
|
||||||
|
public void FetchRooms() => StartCoroutine(DoFetchRooms());
|
||||||
|
|
||||||
|
private IEnumerator DoFetchRooms()
|
||||||
|
{
|
||||||
|
using var req = UnityWebRequest.Get($"{serverURL.Replace("wss://", "https://").Replace("ws://", "http://")}/rooms");
|
||||||
|
yield return req.SendWebRequest();
|
||||||
|
if (req.result != UnityWebRequest.Result.Success) { OnRoomsRefreshed?.Invoke(Array.Empty<RoomInfo>()); yield break; }
|
||||||
|
var wrapper = JsonUtility.FromJson<RoomListWrapper>($"{{\"items\":{req.downloadHandler.text}}}");
|
||||||
|
OnRoomsRefreshed?.Invoke(wrapper?.items?.ToArray() ?? Array.Empty<RoomInfo>());
|
||||||
|
}
|
||||||
|
|
||||||
// --- Events ---
|
// --- Events ---
|
||||||
public event Action OnConnected;
|
public event Action OnConnected;
|
||||||
public event Action OnDisconnected;
|
public event Action OnDisconnected;
|
||||||
@@ -44,10 +64,9 @@ public class NetworkManager : MonoBehaviour
|
|||||||
public event Action<float> OnCountdownChanged; // seconds remaining
|
public event Action<float> OnCountdownChanged; // seconds remaining
|
||||||
public event Action<string, string> OnEliminated; // sessionId, reason
|
public event Action<string, string> OnEliminated; // sessionId, reason
|
||||||
public event Action<string> OnQualified; // sessionId
|
public event Action<string> OnQualified; // sessionId
|
||||||
public event Action<int, string> OnRoundStart; // roundNumber, mode
|
public event Action<int, string, int> OnRoundStart; // roundNumber, mode, totalRounds
|
||||||
public event Action<int> OnRoundEnd; // roundNumber
|
public event Action<int> OnRoundEnd; // roundNumber
|
||||||
public event Action<string> OnGameEnd; // winnerName
|
public event Action<string> OnGameEnd; // winnerName
|
||||||
public event Action<float> OnDeathZoneYChanged; // for survival mode
|
|
||||||
|
|
||||||
// --- Internals ---
|
// --- Internals ---
|
||||||
private Client _client;
|
private Client _client;
|
||||||
@@ -101,93 +120,109 @@ public class NetworkManager : MonoBehaviour
|
|||||||
|
|
||||||
// ─── Join / Leave ────────────────────────────────────────────────────
|
// ─── Join / Leave ────────────────────────────────────────────────────
|
||||||
|
|
||||||
public async void JoinArena(string playerName, Color color)
|
// ─── Join helpers ─────────────────────────────────────────────────────
|
||||||
{
|
|
||||||
if (_isJoining || IsConnected)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("[Network] Already connecting or connected.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private Dictionary<string, object> BuildJoinOptions(string playerName, Color color) => new()
|
||||||
|
{
|
||||||
|
{ "name", playerName },
|
||||||
|
{ "colorR", color.r },
|
||||||
|
{ "colorG", color.g },
|
||||||
|
{ "colorB", color.b },
|
||||||
|
};
|
||||||
|
|
||||||
|
private void PrepareJoin(string playerName, Color color)
|
||||||
|
{
|
||||||
_isJoining = true;
|
_isJoining = true;
|
||||||
ConnectionStatus = "Connexion en cours...";
|
ConnectionStatus = "Connexion en cours...";
|
||||||
LastError = "";
|
LastError = "";
|
||||||
LocalPlayerName = playerName;
|
LocalPlayerName = playerName;
|
||||||
LocalPlayerColor = color;
|
LocalPlayerColor = color;
|
||||||
|
PlayerPrefs.SetString("rolld_player_name", playerName);
|
||||||
|
_client = new Client(serverURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FinishJoin()
|
||||||
|
{
|
||||||
|
LocalSessionId = _room.SessionId;
|
||||||
|
RoomId = _room.RoomId;
|
||||||
|
IsConnected = true;
|
||||||
|
ConnectionStatus = "Connecté";
|
||||||
|
Debug.Log($"[Network] Joined room {RoomId} as {LocalSessionId}");
|
||||||
|
|
||||||
|
_callbacks = Callbacks.Get(_room);
|
||||||
|
_callbacks.OnAdd(state => state.players, (key, player) => OnPlayerAdd(key, player));
|
||||||
|
_callbacks.OnRemove(state => state.players, (key, player) => OnPlayerRemove(key, player));
|
||||||
|
_callbacks.Listen(state => state.phase, (v, _) => _OnPhaseChanged(v));
|
||||||
|
_callbacks.Listen(state => state.countdown, (v, _) => OnCountdownChanged?.Invoke(v));
|
||||||
|
_callbacks.Listen(state => state.playersAlive, (v, _) => GameHUD.Instance?.SetPlayersAlive(v));
|
||||||
|
|
||||||
|
_room.OnMessage<EliminatedMsg>("eliminated", msg => { OnEliminated?.Invoke(msg.sessionId, msg.reason); });
|
||||||
|
_room.OnMessage<QualifiedMsg> ("qualified", msg => { OnQualified?.Invoke(msg.sessionId); });
|
||||||
|
_room.OnMessage<RoundStartMsg>("roundStart", msg => { OnRoundStart?.Invoke(msg.round, msg.mode, msg.totalRounds); });
|
||||||
|
_room.OnMessage<RoundEndMsg> ("roundEnd", msg => { OnRoundEnd?.Invoke(msg.round); });
|
||||||
|
_room.OnMessage<GameEndMsg> ("gameEnd", msg => { OnGameEnd?.Invoke(msg.winner); });
|
||||||
|
_room.OnMessage<ChatUI.ChatMessage>("chat", msg => { ChatUI.Instance?.ReceiveChatMessage(msg); });
|
||||||
|
_room.OnLeave += OnRoomLeave;
|
||||||
|
|
||||||
|
// Seed players already present in the room (state decoded before callbacks were registered)
|
||||||
|
if (_room.State.players != null)
|
||||||
|
{
|
||||||
|
foreach (var kvp in _room.State.players)
|
||||||
|
OnPlayerAdd(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnConnected?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleJoinError(Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[Network] Failed to join: {e.Message}");
|
||||||
|
ConnectionStatus = "Erreur de connexion";
|
||||||
|
LastError = e.Message;
|
||||||
|
IsConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public join methods ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
public async void JoinArena(string playerName, Color color)
|
||||||
|
{
|
||||||
|
if (_isJoining || IsConnected) return;
|
||||||
|
PrepareJoin(playerName, color);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Debug.Log($"[Network] Connecting to {serverURL}...");
|
_room = await _client.JoinOrCreate<NetworkState>("arena", BuildJoinOptions(playerName, color));
|
||||||
_client = new Client(serverURL);
|
FinishJoin();
|
||||||
|
|
||||||
var options = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
{ "name", playerName },
|
|
||||||
{ "colorR", color.r },
|
|
||||||
{ "colorG", color.g },
|
|
||||||
{ "colorB", color.b }
|
|
||||||
};
|
|
||||||
|
|
||||||
_room = await _client.JoinOrCreate<NetworkState>("arena", options);
|
|
||||||
LocalSessionId = _room.SessionId;
|
|
||||||
RoomId = _room.RoomId;
|
|
||||||
IsConnected = true;
|
|
||||||
ConnectionStatus = "Connecté";
|
|
||||||
|
|
||||||
Debug.Log($"[Network] Joined room {RoomId} as {LocalSessionId}");
|
|
||||||
|
|
||||||
_callbacks = Callbacks.Get(_room);
|
|
||||||
|
|
||||||
// Players
|
|
||||||
_callbacks.OnAdd(state => state.players, (key, player) => OnPlayerAdd(key, player));
|
|
||||||
_callbacks.OnRemove(state => state.players, (key, player) => OnPlayerRemove(key, player));
|
|
||||||
|
|
||||||
// Game state changes
|
|
||||||
_callbacks.Listen(state => state.phase, (newValue, prevValue) => _OnPhaseChanged(newValue));
|
|
||||||
_callbacks.Listen(state => state.countdown, (newValue, prevValue) => OnCountdownChanged?.Invoke(newValue));
|
|
||||||
_callbacks.Listen(state => state.deathZoneY, (newValue, prevValue) => OnDeathZoneYChanged?.Invoke(newValue));
|
|
||||||
|
|
||||||
// Server messages
|
|
||||||
_room.OnMessage<EliminatedMsg>("eliminated", msg =>
|
|
||||||
{
|
|
||||||
Debug.Log($"[Network] Eliminated: {msg.sessionId} ({msg.reason})");
|
|
||||||
OnEliminated?.Invoke(msg.sessionId, msg.reason);
|
|
||||||
});
|
|
||||||
_room.OnMessage<QualifiedMsg>("qualified", msg =>
|
|
||||||
{
|
|
||||||
Debug.Log($"[Network] Qualified: {msg.sessionId}");
|
|
||||||
OnQualified?.Invoke(msg.sessionId);
|
|
||||||
});
|
|
||||||
_room.OnMessage<RoundStartMsg>("roundStart", msg =>
|
|
||||||
{
|
|
||||||
Debug.Log($"[Network] Round {msg.round} started ({msg.mode})");
|
|
||||||
OnRoundStart?.Invoke(msg.round, msg.mode);
|
|
||||||
});
|
|
||||||
_room.OnMessage<RoundEndMsg>("roundEnd", msg =>
|
|
||||||
{
|
|
||||||
Debug.Log($"[Network] Round {msg.round} ended");
|
|
||||||
OnRoundEnd?.Invoke(msg.round);
|
|
||||||
});
|
|
||||||
_room.OnMessage<GameEndMsg>("gameEnd", msg =>
|
|
||||||
{
|
|
||||||
Debug.Log($"[Network] Game over — Winner: {msg.winner}");
|
|
||||||
OnGameEnd?.Invoke(msg.winner);
|
|
||||||
});
|
|
||||||
|
|
||||||
_room.OnLeave += OnRoomLeave;
|
|
||||||
OnConnected?.Invoke();
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e) { HandleJoinError(e); }
|
||||||
|
finally { _isJoining = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void JoinByRoomId(string roomId, string playerName, Color color)
|
||||||
|
{
|
||||||
|
if (_isJoining || IsConnected) return;
|
||||||
|
PrepareJoin(playerName, color);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Debug.LogError($"[Network] Failed to join: {e.Message}");
|
_room = await _client.JoinById<NetworkState>(roomId, BuildJoinOptions(playerName, color));
|
||||||
ConnectionStatus = "Erreur de connexion";
|
FinishJoin();
|
||||||
LastError = e.Message;
|
|
||||||
IsConnected = false;
|
|
||||||
}
|
}
|
||||||
finally
|
catch (Exception e) { HandleJoinError(e); }
|
||||||
|
finally { _isJoining = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void CreateRoom(string playerName, Color color, string roomName = null)
|
||||||
|
{
|
||||||
|
if (_isJoining || IsConnected) return;
|
||||||
|
PrepareJoin(playerName, color);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_isJoining = false;
|
var opts = BuildJoinOptions(playerName, color);
|
||||||
|
if (roomName != null) opts["roomName"] = roomName;
|
||||||
|
_room = await _client.Create<NetworkState>("arena", opts);
|
||||||
|
FinishJoin();
|
||||||
}
|
}
|
||||||
|
catch (Exception e) { HandleJoinError(e); }
|
||||||
|
finally { _isJoining = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void LeaveRoom()
|
public async void LeaveRoom()
|
||||||
@@ -208,16 +243,10 @@ public class NetworkManager : MonoBehaviour
|
|||||||
await _room.Send("checkpointReached", new { index });
|
await _room.Send("checkpointReached", new { index });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void SendDeathZoneHit()
|
public async void SendChatMessage(string text)
|
||||||
{
|
{
|
||||||
if (_room != null && IsConnected)
|
if (_room != null && IsConnected)
|
||||||
await _room.Send("deathZoneHit", null);
|
await _room.Send("chat", new { text });
|
||||||
}
|
|
||||||
|
|
||||||
public async void SendInZone(bool inZone)
|
|
||||||
{
|
|
||||||
if (_room != null && IsConnected)
|
|
||||||
await _room.Send("inZone", new { inZone });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── State Callbacks ─────────────────────────────────────────────────
|
// ─── State Callbacks ─────────────────────────────────────────────────
|
||||||
@@ -236,11 +265,14 @@ public class NetworkManager : MonoBehaviour
|
|||||||
PlayerCount = _room.State.players?.Count ?? 0;
|
PlayerCount = _room.State.players?.Count ?? 0;
|
||||||
|
|
||||||
if (sessionId == LocalSessionId) return;
|
if (sessionId == LocalSessionId) return;
|
||||||
|
if (_remotePlayers.ContainsKey(sessionId)) return; // prevent duplicate spawn
|
||||||
|
|
||||||
if (remotePlayerPrefab != null)
|
|
||||||
{
|
{
|
||||||
Vector3 spawnPos = new Vector3(player.x, player.y, player.z);
|
Vector3 spawnPos = new Vector3(player.x, player.y, player.z);
|
||||||
GameObject remoteBall = Instantiate(remotePlayerPrefab, spawnPos, Quaternion.identity);
|
GameObject remoteBall = remotePlayerPrefab != null
|
||||||
|
? Instantiate(remotePlayerPrefab, spawnPos, Quaternion.identity)
|
||||||
|
: GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
||||||
|
remoteBall.transform.position = spawnPos;
|
||||||
remoteBall.name = $"RemotePlayer_{player.name}_{sessionId[..6]}";
|
remoteBall.name = $"RemotePlayer_{player.name}_{sessionId[..6]}";
|
||||||
|
|
||||||
var controller = remoteBall.GetComponent<RemotePlayerController>()
|
var controller = remoteBall.GetComponent<RemotePlayerController>()
|
||||||
@@ -285,11 +317,6 @@ public class NetworkManager : MonoBehaviour
|
|||||||
new Vector3(player.avx, player.avy, player.avz)
|
new Vector3(player.avx, player.avy, player.avz)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sync team color changes (for teams mode)
|
|
||||||
controller.UpdateTeamColor(player.team,
|
|
||||||
new Color(player.colorR, player.colorG, player.colorB));
|
|
||||||
|
|
||||||
// Hide/show eliminated remote players
|
|
||||||
controller.SetVisible(!player.isEliminated);
|
controller.SetVisible(!player.isEliminated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,8 +369,8 @@ public class NetworkManager : MonoBehaviour
|
|||||||
private void OnRoomLeave(int code)
|
private void OnRoomLeave(int code)
|
||||||
{
|
{
|
||||||
Debug.Log($"[Network] Left room (code: {code})");
|
Debug.Log($"[Network] Left room (code: {code})");
|
||||||
|
OnDisconnected?.Invoke(); // before Cleanup so listeners still have LocalPlayerName
|
||||||
Cleanup();
|
Cleanup();
|
||||||
OnDisconnected?.Invoke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Cleanup()
|
private void Cleanup()
|
||||||
|
|||||||
@@ -1,48 +1,57 @@
|
|||||||
using Colyseus.Schema;
|
// Generated from @colyseus/schema 4.0.15 — DO NOT EDIT MANUALLY
|
||||||
|
// Class names kept as NetworkPlayer/NetworkState to match existing codebase references.
|
||||||
|
|
||||||
|
using Colyseus.Schema;
|
||||||
|
#if UNITY_5_3_OR_NEWER
|
||||||
|
using UnityEngine.Scripting;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Must match server-side defineTypes field order exactly
|
|
||||||
public partial class NetworkPlayer : Schema
|
public partial class NetworkPlayer : Schema
|
||||||
{
|
{
|
||||||
[Type(0, "int32")] public int userId = 0;
|
#if UNITY_5_3_OR_NEWER
|
||||||
[Type(1, "float32")] public float x = 0;
|
[Preserve]
|
||||||
[Type(2, "float32")] public float y = 5;
|
#endif
|
||||||
[Type(3, "float32")] public float z = 0;
|
public NetworkPlayer() { }
|
||||||
[Type(4, "float32")] public float vx = 0;
|
|
||||||
[Type(5, "float32")] public float vy = 0;
|
[Type(0, "float32")] public float x = 0;
|
||||||
[Type(6, "float32")] public float vz = 0;
|
[Type(1, "float32")] public float y = 5;
|
||||||
[Type(7, "float32")] public float rx = 0;
|
[Type(2, "float32")] public float z = 0;
|
||||||
[Type(8, "float32")] public float ry = 0;
|
[Type(3, "float32")] public float vx = 0;
|
||||||
[Type(9, "float32")] public float rz = 0;
|
[Type(4, "float32")] public float vy = 0;
|
||||||
[Type(10, "float32")] public float rw = 1;
|
[Type(5, "float32")] public float vz = 0;
|
||||||
[Type(11, "float64")] public double t = 0;
|
[Type(6, "float32")] public float rx = 0;
|
||||||
[Type(12, "string")] public string name = "";
|
[Type(7, "float32")] public float ry = 0;
|
||||||
[Type(13, "float32")] public float colorR = 1;
|
[Type(8, "float32")] public float rz = 0;
|
||||||
[Type(14, "float32")] public float colorG = 1;
|
[Type(9, "float32")] public float rw = 1;
|
||||||
[Type(15, "float32")] public float colorB = 1;
|
[Type(10, "float64")] public double t = 0;
|
||||||
[Type(16, "float32")] public float avx = 0;
|
[Type(11, "string")] public string name = "";
|
||||||
[Type(17, "float32")] public float avy = 0;
|
[Type(12, "float32")] public float colorR = 1;
|
||||||
[Type(18, "float32")] public float avz = 0;
|
[Type(13, "float32")] public float colorG = 1;
|
||||||
// Game state
|
[Type(14, "float32")] public float colorB = 1;
|
||||||
[Type(19, "boolean")] public bool isEliminated = false;
|
[Type(15, "float32")] public float avx = 0;
|
||||||
[Type(20, "boolean")] public bool isQualified = false;
|
[Type(16, "float32")] public float avy = 0;
|
||||||
[Type(21, "int8")] public int team = 0;
|
[Type(17, "float32")] public float avz = 0;
|
||||||
[Type(22, "int8")] public int checkpointIndex = 0;
|
[Type(18, "boolean")] public bool isEliminated = false;
|
||||||
[Type(23, "boolean")] public bool isReady = false;
|
[Type(19, "boolean")] public bool isQualified = false;
|
||||||
|
[Type(20, "boolean")] public bool isReady = false;
|
||||||
|
[Type(21, "int8")] public sbyte checkpointIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class NetworkState : Schema
|
public partial class NetworkState : Schema
|
||||||
{
|
{
|
||||||
[Type(0, "map", typeof(MapSchema<NetworkPlayer>))]
|
#if UNITY_5_3_OR_NEWER
|
||||||
public MapSchema<NetworkPlayer> players;
|
[Preserve]
|
||||||
|
#endif
|
||||||
|
public NetworkState() { }
|
||||||
|
|
||||||
[Type(1, "string")] public string phase = "lobby";
|
[Type(0, "map", typeof(MapSchema<NetworkPlayer>))]
|
||||||
[Type(2, "float32")] public float countdown = 0;
|
public MapSchema<NetworkPlayer> players = null;
|
||||||
[Type(3, "int8")] public int roundNumber = 1;
|
|
||||||
[Type(4, "int8")] public int totalRounds = 4;
|
[Type(1, "string")] public string phase = "lobby";
|
||||||
[Type(5, "int8")] public int playersAlive = 0;
|
[Type(2, "float32")] public float countdown = 0;
|
||||||
[Type(6, "string")] public string gameMode = "race";
|
[Type(3, "int8")] public sbyte roundNumber = 1;
|
||||||
[Type(7, "float32")] public float deathZoneY = -100;
|
[Type(4, "int8")] public sbyte totalRounds = 3;
|
||||||
[Type(8, "int16")] public int teamScoreRed = 0;
|
[Type(5, "int8")] public sbyte playersAlive = 0;
|
||||||
[Type(9, "int16")] public int teamScoreBlue = 0;
|
[Type(6, "string")] public string gameMode = "race";
|
||||||
[Type(10, "string")] public string winnerName = "";
|
[Type(7, "string")] public string winnerName = "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,17 +241,14 @@ public class RemotePlayerController : MonoBehaviour
|
|||||||
transform.rotation = _currentRotation;
|
transform.rotation = _currentRotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep name label floating ABOVE the ball (world position, not local)
|
|
||||||
// Billboard: always face camera, locked to vertical axis
|
|
||||||
if (_nameLabelObj != null)
|
if (_nameLabelObj != null)
|
||||||
{
|
{
|
||||||
_nameLabelObj.transform.position = transform.position + Vector3.up * 1.5f;
|
_nameLabelObj.transform.position = transform.position + Vector3.up * 1.5f;
|
||||||
var cam = Camera.main;
|
var cam = Camera.main;
|
||||||
if (cam != null)
|
if (cam != null)
|
||||||
{
|
{
|
||||||
// Billboard locked to Y axis — only rotate around vertical
|
Vector3 lookDir = cam.transform.position - _nameLabelObj.transform.position;
|
||||||
Vector3 lookDir = _nameLabelObj.transform.position - cam.transform.position;
|
lookDir.y = 0f;
|
||||||
lookDir.y = 0f; // Lock to horizontal plane
|
|
||||||
if (lookDir.sqrMagnitude > 0.001f)
|
if (lookDir.sqrMagnitude > 0.001f)
|
||||||
_nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir);
|
_nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using System.Collections;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages race checkpoints and the finish line.
|
|
||||||
/// Place checkpoint GameObjects in order in the Inspector array.
|
|
||||||
/// Each checkpoint needs a Collider set to "Is Trigger".
|
|
||||||
/// The last checkpoint in the array is treated as the finish line.
|
|
||||||
/// Attach to a persistent manager GameObject in the race scene.
|
|
||||||
/// </summary>
|
|
||||||
public class CheckpointSystem : MonoBehaviour
|
|
||||||
{
|
|
||||||
public static CheckpointSystem Instance { get; private set; }
|
|
||||||
|
|
||||||
[Header("Checkpoints (in order — last one = finish line)")]
|
|
||||||
public Collider[] checkpoints;
|
|
||||||
|
|
||||||
[Header("Visuals")]
|
|
||||||
[Tooltip("Material to apply to active (next) checkpoint")]
|
|
||||||
public Material checkpointActiveMaterial;
|
|
||||||
[Tooltip("Material to apply to passed checkpoints")]
|
|
||||||
public Material checkpointPassedMaterial;
|
|
||||||
[Tooltip("Material to apply to finish line")]
|
|
||||||
public Material finishLineMaterial;
|
|
||||||
|
|
||||||
public int LocalCheckpointIndex => _localCheckpointIndex;
|
|
||||||
public bool RaceStarted { get; private set; }
|
|
||||||
|
|
||||||
private int _localCheckpointIndex = 0;
|
|
||||||
private Renderer[] _checkpointRenderers;
|
|
||||||
private bool _finished = false;
|
|
||||||
|
|
||||||
void Awake()
|
|
||||||
{
|
|
||||||
Instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Start()
|
|
||||||
{
|
|
||||||
_checkpointRenderers = new Renderer[checkpoints.Length];
|
|
||||||
for (int i = 0; i < checkpoints.Length; i++)
|
|
||||||
{
|
|
||||||
_checkpointRenderers[i] = checkpoints[i].GetComponent<Renderer>();
|
|
||||||
checkpoints[i].gameObject.name = $"Checkpoint_{i}";
|
|
||||||
|
|
||||||
// Auto-assign index so trigger knows its position in the sequence
|
|
||||||
var trigger = checkpoints[i].GetComponent<CheckpointTrigger>();
|
|
||||||
if (trigger != null) trigger.checkpointIndex = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell HUD total checkpoints
|
|
||||||
GameHUD.TotalCheckpoints = checkpoints.Length;
|
|
||||||
UpdateCheckpointVisuals();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called by CheckpointTrigger on each checkpoint object.</summary>
|
|
||||||
public void OnLocalPlayerHitCheckpoint(int index)
|
|
||||||
{
|
|
||||||
if (_finished) return;
|
|
||||||
if (index != _localCheckpointIndex) return;
|
|
||||||
|
|
||||||
// CP0 = start gate: activate race HUD and start local timer
|
|
||||||
if (index == 0)
|
|
||||||
{
|
|
||||||
RaceStarted = true;
|
|
||||||
GameHUD.Instance?.SetLocalRaceActive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
_localCheckpointIndex++;
|
|
||||||
NetworkManager.Instance?.SendCheckpoint(_localCheckpointIndex);
|
|
||||||
|
|
||||||
Debug.Log($"[Checkpoint] Reached {_localCheckpointIndex}/{checkpoints.Length}");
|
|
||||||
|
|
||||||
// Update HUD
|
|
||||||
GameHUD.Instance?.SetCheckpoint(_localCheckpointIndex, checkpoints.Length);
|
|
||||||
|
|
||||||
UpdateCheckpointVisuals();
|
|
||||||
|
|
||||||
if (_localCheckpointIndex >= checkpoints.Length)
|
|
||||||
{
|
|
||||||
_finished = true;
|
|
||||||
Debug.Log("[Checkpoint] FINISH LINE reached!");
|
|
||||||
StartCoroutine(FinishFlash());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
StartCoroutine(FlashCheckpoint(index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetForRound()
|
|
||||||
{
|
|
||||||
_localCheckpointIndex = 0;
|
|
||||||
_finished = false;
|
|
||||||
RaceStarted = false;
|
|
||||||
UpdateCheckpointVisuals();
|
|
||||||
GameHUD.Instance?.SetLocalRaceActive(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCheckpointVisuals()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < checkpoints.Length; i++)
|
|
||||||
{
|
|
||||||
if (_checkpointRenderers[i] == null) continue;
|
|
||||||
|
|
||||||
if (i < _localCheckpointIndex)
|
|
||||||
{
|
|
||||||
// Passed
|
|
||||||
if (checkpointPassedMaterial != null)
|
|
||||||
_checkpointRenderers[i].material = checkpointPassedMaterial;
|
|
||||||
_checkpointRenderers[i].enabled = false; // hide passed checkpoints
|
|
||||||
}
|
|
||||||
else if (i == _localCheckpointIndex)
|
|
||||||
{
|
|
||||||
// Active (next to hit)
|
|
||||||
_checkpointRenderers[i].enabled = true;
|
|
||||||
if (i == checkpoints.Length - 1 && finishLineMaterial != null)
|
|
||||||
_checkpointRenderers[i].material = finishLineMaterial;
|
|
||||||
else if (checkpointActiveMaterial != null)
|
|
||||||
_checkpointRenderers[i].material = checkpointActiveMaterial;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Upcoming (not yet active)
|
|
||||||
_checkpointRenderers[i].enabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator FlashCheckpoint(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= checkpoints.Length) yield break;
|
|
||||||
var rend = _checkpointRenderers[index];
|
|
||||||
if (rend == null) yield break;
|
|
||||||
|
|
||||||
Color orig = rend.material.HasProperty("_BaseColor")
|
|
||||||
? rend.material.GetColor("_BaseColor")
|
|
||||||
: rend.material.color;
|
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
|
||||||
{
|
|
||||||
SetRendererColor(rend, new Color(0.3f, 1f, 0.5f));
|
|
||||||
yield return new WaitForSeconds(0.08f);
|
|
||||||
SetRendererColor(rend, orig);
|
|
||||||
yield return new WaitForSeconds(0.08f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator FinishFlash()
|
|
||||||
{
|
|
||||||
float t = 0f;
|
|
||||||
while (t < 2f)
|
|
||||||
{
|
|
||||||
t += Time.deltaTime;
|
|
||||||
yield return null;
|
|
||||||
}
|
|
||||||
// Finish confirmed via network — GameManager/EliminationOverlay handles the "Qualifié!" overlay
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SetRendererColor(Renderer rend, Color c)
|
|
||||||
{
|
|
||||||
if (rend.material.HasProperty("_BaseColor")) rend.material.SetColor("_BaseColor", c);
|
|
||||||
else rend.material.color = c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 5bf5e078a2ee9ed4fa95eacab5753f3a
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attach to each checkpoint GameObject (which must have a trigger Collider).
|
|
||||||
/// Set the checkpointIndex in the Inspector to match the checkpoint's position in the sequence.
|
|
||||||
/// </summary>
|
|
||||||
public class CheckpointTrigger : MonoBehaviour
|
|
||||||
{
|
|
||||||
[Tooltip("Index in the CheckpointSystem.checkpoints array (0-based)")]
|
|
||||||
public int checkpointIndex = 0;
|
|
||||||
|
|
||||||
void OnTriggerEnter(Collider other)
|
|
||||||
{
|
|
||||||
// Only trigger for the local player (has PlayerController)
|
|
||||||
if (other.GetComponent<PlayerController>() == null) return;
|
|
||||||
CheckpointSystem.Instance?.OnLocalPlayerHitCheckpoint(checkpointIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 6d1f3d6aaca8e97498f40d827f7c5216
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: ba062aa6c92b140379dbc06b43dd3b9b
|
guid: e9c4c0760bb30024293b8152d79c595e
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
178
game/Assets/Scripts/Stats/StatsTracker.cs
Normal file
178
game/Assets/Scripts/Stats/StatsTracker.cs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks player statistics and uploads them to the game server every 30s + on disconnect.
|
||||||
|
/// No dependency on round events — works even if Colyseus callbacks are broken.
|
||||||
|
/// </summary>
|
||||||
|
public class StatsTracker : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static StatsTracker Instance { get; private set; }
|
||||||
|
|
||||||
|
private const string SERVER_URL = "https://game.rolld.kerboul.me";
|
||||||
|
private const float SEND_INTERVAL = 30f;
|
||||||
|
private const float MIN_SEND_INTERVAL = 6f; // juste au-dessus du rate-limit serveur (5s)
|
||||||
|
|
||||||
|
// Cumulative stats
|
||||||
|
private float _totalDistance;
|
||||||
|
private int _totalJumps;
|
||||||
|
private float _maxSpeed;
|
||||||
|
private int _bumpsGiven;
|
||||||
|
|
||||||
|
// Playtime
|
||||||
|
private float _sessionStart;
|
||||||
|
private float _playtimeSentSoFar; // how much playtime we already sent
|
||||||
|
|
||||||
|
// Tracking
|
||||||
|
private Vector3 _lastPos;
|
||||||
|
private bool _tracking;
|
||||||
|
private string _cachedName = "";
|
||||||
|
private float _lastSentTime = -999f;
|
||||||
|
|
||||||
|
private PlayerController _pc;
|
||||||
|
private Rigidbody _rb;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
_pc = GetComponent<PlayerController>();
|
||||||
|
_rb = GetComponent<Rigidbody>();
|
||||||
|
|
||||||
|
var nm = NetworkManager.Instance;
|
||||||
|
if (nm != null)
|
||||||
|
{
|
||||||
|
nm.OnConnected += OnConnected;
|
||||||
|
nm.OnDisconnected += OnDisconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
var nm = NetworkManager.Instance;
|
||||||
|
if (nm != null)
|
||||||
|
{
|
||||||
|
nm.OnConnected -= OnConnected;
|
||||||
|
nm.OnDisconnected -= OnDisconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (!_tracking || _rb == null) return;
|
||||||
|
|
||||||
|
Vector3 pos = transform.position;
|
||||||
|
float delta = Vector3.Distance(pos, _lastPos);
|
||||||
|
if (delta < 20f) // filtre téléportations
|
||||||
|
_totalDistance += delta;
|
||||||
|
_lastPos = pos;
|
||||||
|
|
||||||
|
float speed = _rb.linearVelocity.magnitude;
|
||||||
|
if (speed > _maxSpeed) _maxSpeed = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public hooks ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void RegisterJump() => _totalJumps++;
|
||||||
|
public void RegisterBump() => _bumpsGiven++;
|
||||||
|
|
||||||
|
// ─── Connection events ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void OnConnected()
|
||||||
|
{
|
||||||
|
_cachedName = NetworkManager.Instance?.LocalPlayerName ?? "";
|
||||||
|
_lastPos = transform.position;
|
||||||
|
_sessionStart = Time.time;
|
||||||
|
_tracking = true;
|
||||||
|
StartCoroutine(PeriodicSend());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisconnected()
|
||||||
|
{
|
||||||
|
_tracking = false;
|
||||||
|
StopAllCoroutines();
|
||||||
|
SendStats(); // envoi final best-effort
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Periodic send ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private IEnumerator PeriodicSend()
|
||||||
|
{
|
||||||
|
while (_tracking)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(SEND_INTERVAL);
|
||||||
|
if (_tracking) SendStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── HTTP send ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void SendStats()
|
||||||
|
{
|
||||||
|
if (Time.time - _lastSentTime < MIN_SEND_INTERVAL) return;
|
||||||
|
var nm = NetworkManager.Instance;
|
||||||
|
string name = (nm != null && !string.IsNullOrEmpty(nm.LocalPlayerName))
|
||||||
|
? nm.LocalPlayerName
|
||||||
|
: _cachedName;
|
||||||
|
if (string.IsNullOrEmpty(name)) return;
|
||||||
|
_lastSentTime = Time.time;
|
||||||
|
StartCoroutine(DoSendStats(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator DoSendStats(string playerName)
|
||||||
|
{
|
||||||
|
float now = Time.time;
|
||||||
|
float sessionSecs = now - _sessionStart;
|
||||||
|
float playtimeToSend = sessionSecs - _playtimeSentSoFar;
|
||||||
|
_playtimeSentSoFar = sessionSecs;
|
||||||
|
|
||||||
|
var payload = new StatsPayload
|
||||||
|
{
|
||||||
|
name = playerName,
|
||||||
|
stats = new StatsData
|
||||||
|
{
|
||||||
|
totalDistance = _totalDistance,
|
||||||
|
totalJumps = _totalJumps,
|
||||||
|
maxSpeed = _maxSpeed,
|
||||||
|
bumpsGiven = _bumpsGiven,
|
||||||
|
totalPlaytime = playtimeToSend,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
string json = JsonUtility.ToJson(payload);
|
||||||
|
byte[] body = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
|
using var req = new UnityWebRequest($"{SERVER_URL}/stats/update", "POST");
|
||||||
|
req.uploadHandler = new UploadHandlerRaw(body);
|
||||||
|
req.downloadHandler = new DownloadHandlerBuffer();
|
||||||
|
req.SetRequestHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
|
yield return req.SendWebRequest();
|
||||||
|
|
||||||
|
if (req.result != UnityWebRequest.Result.Success)
|
||||||
|
Debug.LogWarning($"[Stats] Upload failed: {req.error}");
|
||||||
|
else
|
||||||
|
Debug.Log($"[Stats] Sent for {playerName} — dist:{_totalDistance:F0}m spd:{_maxSpeed:F1}m/s jumps:{_totalJumps}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DTOs ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
private class StatsPayload { public string name; public StatsData stats; }
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
private class StatsData
|
||||||
|
{
|
||||||
|
public float totalDistance;
|
||||||
|
public int totalJumps;
|
||||||
|
public float maxSpeed;
|
||||||
|
public int bumpsGiven;
|
||||||
|
public float totalPlaytime;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
game/Assets/Scripts/Stats/StatsTracker.cs.meta
Normal file
2
game/Assets/Scripts/Stats/StatsTracker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f63c62c1f3656c644947907edfe2e07f
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Survival mode: a rising death zone that climbs from below.
|
|
||||||
/// The server is authoritative on the Y position (broadcast via NetworkState.deathZoneY).
|
|
||||||
/// This component moves the visual/collider locally, and detects local player contact.
|
|
||||||
///
|
|
||||||
/// Setup: Create a large Plane or Cube GameObject, attach this script,
|
|
||||||
/// add a Box/Mesh Collider set to "Is Trigger".
|
|
||||||
/// The object will be positioned at deathZoneY each frame.
|
|
||||||
/// </summary>
|
|
||||||
public class DeathZone : MonoBehaviour
|
|
||||||
{
|
|
||||||
[Header("Visual")]
|
|
||||||
[Tooltip("Half-size of the death zone plane (X and Z)")]
|
|
||||||
public float halfExtent = 200f;
|
|
||||||
|
|
||||||
[Tooltip("Thickness of the zone collider")]
|
|
||||||
public float thickness = 2f;
|
|
||||||
|
|
||||||
[Header("Warning")]
|
|
||||||
[Tooltip("Distance above death zone where the red tint starts")]
|
|
||||||
public float warningDistance = 8f;
|
|
||||||
|
|
||||||
private bool _hitSent = false;
|
|
||||||
private float _targetY = -100f;
|
|
||||||
|
|
||||||
void Start()
|
|
||||||
{
|
|
||||||
// Scale the collider to cover the arena
|
|
||||||
transform.localScale = new Vector3(halfExtent * 2f, thickness, halfExtent * 2f);
|
|
||||||
|
|
||||||
// Subscribe to death zone Y changes from server
|
|
||||||
if (NetworkManager.Instance != null)
|
|
||||||
{
|
|
||||||
NetworkManager.Instance.OnDeathZoneYChanged += OnDeathZoneYChanged;
|
|
||||||
NetworkManager.Instance.OnPhaseChanged += OnPhaseChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnDestroy()
|
|
||||||
{
|
|
||||||
if (NetworkManager.Instance != null)
|
|
||||||
{
|
|
||||||
NetworkManager.Instance.OnDeathZoneYChanged -= OnDeathZoneYChanged;
|
|
||||||
NetworkManager.Instance.OnPhaseChanged -= OnPhaseChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnDeathZoneYChanged(float y)
|
|
||||||
{
|
|
||||||
_targetY = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnPhaseChanged(string phase)
|
|
||||||
{
|
|
||||||
if (phase == "playing" || phase == "lobby")
|
|
||||||
{
|
|
||||||
_hitSent = false; // reset for new round
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Update()
|
|
||||||
{
|
|
||||||
// Smooth follow of the server Y value
|
|
||||||
Vector3 pos = transform.position;
|
|
||||||
pos.y = Mathf.Lerp(pos.y, _targetY, Time.deltaTime * 3f);
|
|
||||||
transform.position = pos;
|
|
||||||
|
|
||||||
// Update warning tint based on local player proximity
|
|
||||||
var nm = NetworkManager.Instance;
|
|
||||||
if (nm != null && nm.IsConnected && GameHUD.Instance != null)
|
|
||||||
{
|
|
||||||
var localState = nm.GetLocalPlayerState();
|
|
||||||
if (localState != null)
|
|
||||||
{
|
|
||||||
float dist = localState.y - pos.y;
|
|
||||||
float intensity = Mathf.Clamp01(1f - dist / warningDistance);
|
|
||||||
GameHUD.Instance.SetDeathZoneWarning(intensity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnTriggerEnter(Collider other)
|
|
||||||
{
|
|
||||||
if (_hitSent) return;
|
|
||||||
if (other.GetComponent<PlayerController>() == null) return;
|
|
||||||
|
|
||||||
_hitSent = true;
|
|
||||||
Debug.Log("[DeathZone] Local player hit the death zone!");
|
|
||||||
NetworkManager.Instance?.SendDeathZoneHit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 99fdfaa3e87a64d4e958f81014e6cdab
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Teams mode: a central capture zone. The local player sends "inZone" messages
|
|
||||||
/// to the server while inside the zone. The server tallies scores.
|
|
||||||
/// Visualizes zone control by tinting the zone's renderer.
|
|
||||||
///
|
|
||||||
/// Setup: Create a flat Box/Plane in the center of the arena.
|
|
||||||
/// Add a BoxCollider set to "Is Trigger". Attach this script.
|
|
||||||
/// </summary>
|
|
||||||
public class ZoneCapture : MonoBehaviour
|
|
||||||
{
|
|
||||||
public static ZoneCapture Instance { get; private set; }
|
|
||||||
|
|
||||||
[Header("Visual")]
|
|
||||||
[Tooltip("Neutral zone color")]
|
|
||||||
public Color neutralColor = new Color(0.5f, 0.5f, 0.6f, 0.5f);
|
|
||||||
[Tooltip("Red team controls color")]
|
|
||||||
public Color redColor = new Color(1f, 0.2f, 0.2f, 0.6f);
|
|
||||||
[Tooltip("Blue team controls color")]
|
|
||||||
public Color blueColor = new Color(0.2f, 0.5f, 1f, 0.6f);
|
|
||||||
|
|
||||||
[Header("Score Reporting")]
|
|
||||||
[Tooltip("How often (seconds) to send inZone=true to server while inside")]
|
|
||||||
public float reportInterval = 0.5f;
|
|
||||||
|
|
||||||
private bool _isLocalPlayerInZone = false;
|
|
||||||
private float _reportTimer = 0f;
|
|
||||||
private Renderer _renderer;
|
|
||||||
|
|
||||||
// Zone occupant counts (received via server state — approximated by remote player positions)
|
|
||||||
private int _redInZone = 0;
|
|
||||||
private int _blueInZone = 0;
|
|
||||||
|
|
||||||
void Awake()
|
|
||||||
{
|
|
||||||
Instance = this;
|
|
||||||
_renderer = GetComponent<Renderer>();
|
|
||||||
SetZoneColor(neutralColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Start()
|
|
||||||
{
|
|
||||||
if (NetworkManager.Instance != null)
|
|
||||||
{
|
|
||||||
NetworkManager.Instance.OnPhaseChanged += OnPhaseChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnDestroy()
|
|
||||||
{
|
|
||||||
if (NetworkManager.Instance != null)
|
|
||||||
NetworkManager.Instance.OnPhaseChanged -= OnPhaseChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnPhaseChanged(string phase)
|
|
||||||
{
|
|
||||||
if (phase == "lobby" || phase == "roundEnd")
|
|
||||||
{
|
|
||||||
_isLocalPlayerInZone = false;
|
|
||||||
_redInZone = 0;
|
|
||||||
_blueInZone = 0;
|
|
||||||
SetZoneColor(neutralColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Update()
|
|
||||||
{
|
|
||||||
if (!_isLocalPlayerInZone) return;
|
|
||||||
|
|
||||||
_reportTimer += Time.deltaTime;
|
|
||||||
if (_reportTimer >= reportInterval)
|
|
||||||
{
|
|
||||||
_reportTimer = 0f;
|
|
||||||
NetworkManager.Instance?.SendInZone(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update zone tint based on dominance
|
|
||||||
UpdateZoneColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnTriggerEnter(Collider other)
|
|
||||||
{
|
|
||||||
if (other.GetComponent<PlayerController>() != null)
|
|
||||||
{
|
|
||||||
_isLocalPlayerInZone = true;
|
|
||||||
_reportTimer = 0f;
|
|
||||||
NetworkManager.Instance?.SendInZone(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count remote players in zone
|
|
||||||
var remote = other.GetComponent<RemotePlayerController>();
|
|
||||||
if (remote != null)
|
|
||||||
{
|
|
||||||
// team info would need to be tracked — skip for now, server handles scoring
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnTriggerExit(Collider other)
|
|
||||||
{
|
|
||||||
if (other.GetComponent<PlayerController>() != null)
|
|
||||||
{
|
|
||||||
_isLocalPlayerInZone = false;
|
|
||||||
NetworkManager.Instance?.SendInZone(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateZoneColor()
|
|
||||||
{
|
|
||||||
// Read team scores from NetworkManager state for visual feedback
|
|
||||||
// For now use a pulsing neutral tint when local player is inside
|
|
||||||
float pulse = 0.7f + 0.3f * Mathf.Sin(Time.time * 3f);
|
|
||||||
Color c = neutralColor;
|
|
||||||
c.a = pulse * 0.6f;
|
|
||||||
SetZoneColor(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetZoneColor(Color c)
|
|
||||||
{
|
|
||||||
if (_renderer == null) return;
|
|
||||||
var mat = _renderer.material;
|
|
||||||
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", c);
|
|
||||||
else mat.color = c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 3e7c98b369c3ccf4aac0ad3ad2bcbbff
|
|
||||||
269
game/Assets/Scripts/UI/ChatUI.cs
Normal file
269
game/Assets/Scripts/UI/ChatUI.cs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// General chat panel. T to open, Escape to close.
|
||||||
|
/// Polls GET /chat/history every 3s and sends via POST /chat/send (or Colyseus if connected).
|
||||||
|
/// Uses ImGuiSkin for visual consistency.
|
||||||
|
/// </summary>
|
||||||
|
public class ChatUI : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static ChatUI Instance { get; private set; }
|
||||||
|
public static bool IsVisible { get; private set; }
|
||||||
|
|
||||||
|
private const string SERVER_URL = "https://game.rolld.kerboul.me";
|
||||||
|
private const float POLL_INTERVAL = 3f;
|
||||||
|
private const int MAX_DISPLAY = 50;
|
||||||
|
|
||||||
|
private bool _visible;
|
||||||
|
private string _inputText = "";
|
||||||
|
private Vector2 _scrollPos;
|
||||||
|
private float _pollTimer;
|
||||||
|
private long _lastTimestamp;
|
||||||
|
private bool _autoScroll = true;
|
||||||
|
|
||||||
|
private readonly List<ChatMessage> _messages = new();
|
||||||
|
private int _unreadCount;
|
||||||
|
|
||||||
|
// Cached textures for badge
|
||||||
|
private static Texture2D _badgeTex;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
||||||
|
Instance = this;
|
||||||
|
// Initial load
|
||||||
|
StartCoroutine(DoPoll());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (Keyboard.current != null)
|
||||||
|
{
|
||||||
|
if (!_visible && Keyboard.current[Key.T].wasPressedThisFrame)
|
||||||
|
Toggle();
|
||||||
|
else if (_visible && Keyboard.current[Key.Escape].wasPressedThisFrame)
|
||||||
|
Toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_visible)
|
||||||
|
{
|
||||||
|
_pollTimer += Time.deltaTime;
|
||||||
|
if (_pollTimer >= POLL_INTERVAL) { _pollTimer = 0f; StartCoroutine(DoPoll()); }
|
||||||
|
|
||||||
|
// Send on Enter (only when chat input has text)
|
||||||
|
if (!string.IsNullOrWhiteSpace(_inputText) &&
|
||||||
|
Keyboard.current != null && Keyboard.current[Key.Enter].wasPressedThisFrame)
|
||||||
|
{
|
||||||
|
TrySend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Toggle()
|
||||||
|
{
|
||||||
|
_visible = !_visible;
|
||||||
|
IsVisible = _visible;
|
||||||
|
|
||||||
|
if (_visible)
|
||||||
|
{
|
||||||
|
_unreadCount = 0;
|
||||||
|
_autoScroll = true;
|
||||||
|
_pollTimer = POLL_INTERVAL; // poll immediately
|
||||||
|
Cursor.lockState = CursorLockMode.None;
|
||||||
|
Cursor.visible = true;
|
||||||
|
// Release held movement keys so the ball doesn't keep moving while typing
|
||||||
|
FindFirstObjectByType<PlayerController>()?.ResetInputs();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Only re-lock if no other UI is open
|
||||||
|
if (!KeyBindingUI.IsVisible)
|
||||||
|
{
|
||||||
|
Cursor.lockState = CursorLockMode.Locked;
|
||||||
|
Cursor.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
if (!_visible)
|
||||||
|
{
|
||||||
|
DrawBadge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiSkin.EnsureReady();
|
||||||
|
|
||||||
|
// Panel bottom-right, doesn't obstruct the center
|
||||||
|
float w = 460f;
|
||||||
|
float h = 440f;
|
||||||
|
float x = Screen.width - w - 12f;
|
||||||
|
float y = Screen.height - h - 12f;
|
||||||
|
|
||||||
|
ImGuiSkin.BeginWindowAt(x, y, w, h, "CHAT GÉNÉRAL");
|
||||||
|
|
||||||
|
// ── Message history ───────────────────────────────────────────
|
||||||
|
float listH = h - 130f;
|
||||||
|
_scrollPos = GUILayout.BeginScrollView(_scrollPos, ImGuiSkin.ScrollView, GUILayout.Height(listH));
|
||||||
|
|
||||||
|
foreach (var msg in _messages)
|
||||||
|
{
|
||||||
|
var ts = System.DateTimeOffset.FromUnixTimeMilliseconds(msg.timestamp).ToLocalTime();
|
||||||
|
string timeStr = ts.ToString("HH:mm");
|
||||||
|
|
||||||
|
var timeStyle = new GUIStyle(ImGuiSkin.LabelDim) { fontSize = 10, fixedWidth = 36f };
|
||||||
|
var nameStyle = new GUIStyle(ImGuiSkin.LabelBold);
|
||||||
|
nameStyle.normal.textColor = ImGuiSkin.ColAccent;
|
||||||
|
var textStyle = new GUIStyle(ImGuiSkin.LabelRich);
|
||||||
|
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
GUILayout.Label(timeStr, timeStyle);
|
||||||
|
GUILayout.Label(msg.name + " :", nameStyle, GUILayout.Width(100f));
|
||||||
|
GUILayout.Label(msg.text, textStyle);
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
GUILayout.Space(1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_autoScroll) _scrollPos.y = float.MaxValue;
|
||||||
|
GUILayout.EndScrollView();
|
||||||
|
|
||||||
|
ImGuiSkin.Separator();
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
|
||||||
|
// ── Input row ────────────────────────────────────────────────
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
GUI.SetNextControlName("ChatInput");
|
||||||
|
_inputText = GUILayout.TextField(_inputText, 200, ImGuiSkin.TextField, GUILayout.Height(28f));
|
||||||
|
|
||||||
|
bool canSend = !string.IsNullOrWhiteSpace(_inputText) && PlayerName.Length > 0;
|
||||||
|
if (PlayerName.Length == 0)
|
||||||
|
GUILayout.Label("Connectez-vous au jeu pour envoyer des messages.", ImGuiSkin.LabelDim);
|
||||||
|
GUI.enabled = canSend;
|
||||||
|
if (GUILayout.Button("Envoyer", ImGuiSkin.Button, GUILayout.Width(80f), GUILayout.Height(28f)))
|
||||||
|
TrySend();
|
||||||
|
GUI.enabled = true;
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
GUILayout.Label("T — Ouvrir · Échap — Fermer · Entrée — Envoyer", ImGuiSkin.Footer);
|
||||||
|
|
||||||
|
ImGuiSkin.EndWindow();
|
||||||
|
|
||||||
|
// Auto-focus input field
|
||||||
|
GUI.FocusControl("ChatInput");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawBadge()
|
||||||
|
{
|
||||||
|
if (_unreadCount <= 0) return;
|
||||||
|
if (_badgeTex == null)
|
||||||
|
{
|
||||||
|
_badgeTex = new Texture2D(1, 1);
|
||||||
|
_badgeTex.SetPixel(0, 0, Color.white);
|
||||||
|
_badgeTex.Apply();
|
||||||
|
}
|
||||||
|
float bx = Screen.width - 68f;
|
||||||
|
float by = Screen.height - 32f;
|
||||||
|
GUI.color = new Color(0.9f, 0.2f, 0.2f, 0.9f);
|
||||||
|
GUI.DrawTexture(new Rect(bx, by, 56f, 22f), _badgeTex);
|
||||||
|
GUI.color = Color.white;
|
||||||
|
var s = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 11, fontStyle = FontStyle.Bold };
|
||||||
|
s.normal.textColor = Color.white;
|
||||||
|
GUI.Label(new Rect(bx, by, 56f, 22f), $"💬 {_unreadCount}", s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Send ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private string PlayerName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var nm = NetworkManager.Instance?.LocalPlayerName;
|
||||||
|
if (!string.IsNullOrEmpty(nm)) return nm;
|
||||||
|
return PlayerPrefs.GetString("rolld_player_name", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrySend()
|
||||||
|
{
|
||||||
|
string text = _inputText.Trim();
|
||||||
|
if (string.IsNullOrEmpty(text) || PlayerName.Length == 0) return;
|
||||||
|
_inputText = "";
|
||||||
|
_autoScroll = true;
|
||||||
|
// Always use HTTP — the server endpoint broadcasts to Colyseus rooms anyway
|
||||||
|
StartCoroutine(DoSend(PlayerName, text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── HTTP polling ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private IEnumerator DoPoll()
|
||||||
|
{
|
||||||
|
string url = $"{SERVER_URL}/chat/history?since={_lastTimestamp}";
|
||||||
|
using var req = UnityWebRequest.Get(url);
|
||||||
|
yield return req.SendWebRequest();
|
||||||
|
if (req.result != UnityWebRequest.Result.Success) yield break;
|
||||||
|
|
||||||
|
var wrapper = JsonUtility.FromJson<MessageListWrapper>($"{{\"items\":{req.downloadHandler.text}}}");
|
||||||
|
if (wrapper?.items == null) yield break;
|
||||||
|
|
||||||
|
int added = 0;
|
||||||
|
foreach (var msg in wrapper.items)
|
||||||
|
{
|
||||||
|
if (msg.timestamp > _lastTimestamp)
|
||||||
|
{
|
||||||
|
_messages.Add(msg);
|
||||||
|
_lastTimestamp = msg.timestamp;
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_messages.Count > MAX_DISPLAY)
|
||||||
|
_messages.RemoveRange(0, _messages.Count - MAX_DISPLAY);
|
||||||
|
|
||||||
|
if (added > 0 && !_visible)
|
||||||
|
_unreadCount += added;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator DoSend(string name, string text)
|
||||||
|
{
|
||||||
|
var payload = new SendPayload { name = name, text = text };
|
||||||
|
string json = JsonUtility.ToJson(payload);
|
||||||
|
byte[] body = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
|
using var req = new UnityWebRequest($"{SERVER_URL}/chat/send", "POST");
|
||||||
|
req.uploadHandler = new UploadHandlerRaw(body);
|
||||||
|
req.downloadHandler = new DownloadHandlerBuffer();
|
||||||
|
req.SetRequestHeader("Content-Type", "application/json");
|
||||||
|
yield return req.SendWebRequest();
|
||||||
|
|
||||||
|
// Immediate poll so the message appears right away
|
||||||
|
StartCoroutine(DoPoll());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by NetworkManager when a "chat" message arrives via Colyseus
|
||||||
|
public void ReceiveChatMessage(ChatMessage msg)
|
||||||
|
{
|
||||||
|
if (_messages.Count >= MAX_DISPLAY)
|
||||||
|
_messages.RemoveAt(0);
|
||||||
|
_messages.Add(msg);
|
||||||
|
if (msg.timestamp > _lastTimestamp) _lastTimestamp = msg.timestamp;
|
||||||
|
if (!_visible) _unreadCount++;
|
||||||
|
_autoScroll = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DTOs ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
public class ChatMessage { public int id; public long timestamp; public string name; public string text; }
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
private class SendPayload { public string name; public string text; }
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
private class MessageListWrapper { public List<ChatMessage> items; }
|
||||||
|
}
|
||||||
2
game/Assets/Scripts/UI/ChatUI.cs.meta
Normal file
2
game/Assets/Scripts/UI/ChatUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: baad3cbcb6161a548ba18d5272ba57c5
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Full-screen overlay for elimination, qualification, and game-end events.
|
|
||||||
/// Fade in → hold → fade out automatically.
|
|
||||||
/// </summary>
|
|
||||||
public class EliminationOverlay : MonoBehaviour
|
|
||||||
{
|
|
||||||
private enum OverlayType { None, Eliminated, Qualified, GameEnd }
|
|
||||||
|
|
||||||
private OverlayType _type = OverlayType.None;
|
|
||||||
private float _alpha = 0f;
|
|
||||||
private string _winnerName = "";
|
|
||||||
|
|
||||||
private static Texture2D _bgTex;
|
|
||||||
|
|
||||||
void Start()
|
|
||||||
{
|
|
||||||
if (_bgTex == null)
|
|
||||||
{
|
|
||||||
_bgTex = new Texture2D(1, 1);
|
|
||||||
_bgTex.SetPixel(0, 0, Color.white);
|
|
||||||
_bgTex.Apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowEliminated() => StartCoroutine(ShowOverlay(OverlayType.Eliminated, 3f));
|
|
||||||
public void ShowQualified() => StartCoroutine(ShowOverlay(OverlayType.Qualified, 2.5f));
|
|
||||||
public void ShowGameEnd(string winner)
|
|
||||||
{
|
|
||||||
_winnerName = winner;
|
|
||||||
StartCoroutine(ShowOverlay(OverlayType.GameEnd, 6f));
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator ShowOverlay(OverlayType type, float holdTime)
|
|
||||||
{
|
|
||||||
_type = type;
|
|
||||||
|
|
||||||
// Fade in
|
|
||||||
float t = 0f;
|
|
||||||
while (t < 0.3f)
|
|
||||||
{
|
|
||||||
t += Time.deltaTime;
|
|
||||||
_alpha = Mathf.Clamp01(t / 0.3f);
|
|
||||||
yield return null;
|
|
||||||
}
|
|
||||||
_alpha = 1f;
|
|
||||||
|
|
||||||
// Hold
|
|
||||||
yield return new WaitForSeconds(holdTime);
|
|
||||||
|
|
||||||
// Fade out
|
|
||||||
t = 0f;
|
|
||||||
while (t < 0.4f)
|
|
||||||
{
|
|
||||||
t += Time.deltaTime;
|
|
||||||
_alpha = 1f - Mathf.Clamp01(t / 0.4f);
|
|
||||||
yield return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_alpha = 0f;
|
|
||||||
_type = OverlayType.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnGUI()
|
|
||||||
{
|
|
||||||
if (_type == OverlayType.None || _alpha < 0.01f) return;
|
|
||||||
|
|
||||||
// Background tint
|
|
||||||
Color bgColor = _type switch
|
|
||||||
{
|
|
||||||
OverlayType.Eliminated => new Color(0.7f, 0.05f, 0.05f, _alpha * 0.55f),
|
|
||||||
OverlayType.Qualified => new Color(0.05f, 0.55f, 0.15f, _alpha * 0.45f),
|
|
||||||
OverlayType.GameEnd => new Color(0.05f, 0.05f, 0.3f, _alpha * 0.6f),
|
|
||||||
_ => Color.clear
|
|
||||||
};
|
|
||||||
GUI.color = bgColor;
|
|
||||||
GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), _bgTex);
|
|
||||||
GUI.color = Color.white;
|
|
||||||
|
|
||||||
// Main text
|
|
||||||
var mainStyle = new GUIStyle(GUI.skin.label)
|
|
||||||
{
|
|
||||||
alignment = TextAnchor.MiddleCenter,
|
|
||||||
fontSize = 72,
|
|
||||||
fontStyle = FontStyle.Bold,
|
|
||||||
};
|
|
||||||
|
|
||||||
string mainText = _type switch
|
|
||||||
{
|
|
||||||
OverlayType.Eliminated => "ÉLIMINÉ !",
|
|
||||||
OverlayType.Qualified => "QUALIFIÉ !",
|
|
||||||
OverlayType.GameEnd => "VICTOIRE !",
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
|
|
||||||
Color textColor = _type switch
|
|
||||||
{
|
|
||||||
OverlayType.Eliminated => new Color(1f, 0.3f, 0.2f, _alpha),
|
|
||||||
OverlayType.Qualified => new Color(0.3f, 1f, 0.5f, _alpha),
|
|
||||||
OverlayType.GameEnd => new Color(1f, 0.85f, 0.1f, _alpha),
|
|
||||||
_ => Color.clear
|
|
||||||
};
|
|
||||||
mainStyle.normal.textColor = textColor;
|
|
||||||
GUI.Label(new Rect(0, Screen.height * 0.35f, Screen.width, 100f), mainText, mainStyle);
|
|
||||||
|
|
||||||
// Sub text
|
|
||||||
var subStyle = new GUIStyle(GUI.skin.label)
|
|
||||||
{
|
|
||||||
alignment = TextAnchor.MiddleCenter,
|
|
||||||
fontSize = 26,
|
|
||||||
fontStyle = FontStyle.Bold,
|
|
||||||
};
|
|
||||||
subStyle.normal.textColor = new Color(1f, 1f, 1f, _alpha * 0.85f);
|
|
||||||
|
|
||||||
string subText = _type switch
|
|
||||||
{
|
|
||||||
OverlayType.Eliminated => "Meilleure chance la prochaine fois !",
|
|
||||||
OverlayType.Qualified => "Tu passes au round suivant !",
|
|
||||||
OverlayType.GameEnd => $"Gagnant : {_winnerName}",
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
GUI.Label(new Rect(0, Screen.height * 0.35f + 100f, Screen.width, 50f), subText, subStyle);
|
|
||||||
|
|
||||||
// Emoji accent
|
|
||||||
var emojiStyle = new GUIStyle(GUI.skin.label)
|
|
||||||
{
|
|
||||||
alignment = TextAnchor.MiddleCenter,
|
|
||||||
fontSize = 48,
|
|
||||||
};
|
|
||||||
emojiStyle.normal.textColor = new Color(1f, 1f, 1f, _alpha * 0.7f);
|
|
||||||
string emoji = _type switch
|
|
||||||
{
|
|
||||||
OverlayType.Eliminated => "💀",
|
|
||||||
OverlayType.Qualified => "✅",
|
|
||||||
OverlayType.GameEnd => "🏆",
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
GUI.Label(new Rect(0, Screen.height * 0.35f - 80f, Screen.width, 70f), emoji, emojiStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 51e21afb9dba1904bb425ac1fae825cb
|
|
||||||
@@ -16,9 +16,6 @@ public class GameHUD : MonoBehaviour
|
|||||||
private float _roundTimer = 0f;
|
private float _roundTimer = 0f;
|
||||||
private bool _timerRunning = false;
|
private bool _timerRunning = false;
|
||||||
|
|
||||||
// Checkpoint info (set by CheckpointSystem)
|
|
||||||
private int _checkpointsCurrent = 0;
|
|
||||||
private int _checkpointsTotal = 5;
|
|
||||||
|
|
||||||
// Local race state (activated when CP0 gate is crossed, independent of server phase)
|
// Local race state (activated when CP0 gate is crossed, independent of server phase)
|
||||||
private bool _localRaceActive = false;
|
private bool _localRaceActive = false;
|
||||||
@@ -57,13 +54,13 @@ public class GameHUD : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnRoundStart(int round, string mode)
|
void OnRoundStart(int round, string mode, int totalRounds)
|
||||||
{
|
{
|
||||||
_roundNumber = round;
|
_roundNumber = round;
|
||||||
|
_totalRounds = totalRounds;
|
||||||
_gameMode = mode;
|
_gameMode = mode;
|
||||||
_roundTimer = 0f;
|
_roundTimer = 0f;
|
||||||
_timerRunning = true;
|
_timerRunning = true;
|
||||||
_checkpointsCurrent = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnPhaseChanged(string phase)
|
void OnPhaseChanged(string phase)
|
||||||
@@ -88,10 +85,13 @@ public class GameHUD : MonoBehaviour
|
|||||||
_countdownPulse = Mathf.Max(0f, _countdownPulse - Time.deltaTime * 3f);
|
_countdownPulse = Mathf.Max(0f, _countdownPulse - Time.deltaTime * 3f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public float LocalRaceTimer => _localRaceTimer;
|
||||||
|
|
||||||
public void SetPhase(string phase) => _phase = phase;
|
public void SetPhase(string phase) => _phase = phase;
|
||||||
public void SetCountdown(float v) => _countdown = v;
|
public void SetCountdown(float v) => _countdown = v;
|
||||||
public void SetRoundInfo(int round, string mode) { _roundNumber = round; _gameMode = mode; }
|
public void SetRoundInfo(int round, string mode) { _roundNumber = round; _gameMode = mode; }
|
||||||
public void SetCheckpoint(int current, int total) { _checkpointsCurrent = current; _checkpointsTotal = total; }
|
public void SetTotalRounds(int n) => _totalRounds = n;
|
||||||
|
|
||||||
|
|
||||||
public void SetLocalRaceActive(bool active)
|
public void SetLocalRaceActive(bool active)
|
||||||
{
|
{
|
||||||
@@ -168,15 +168,6 @@ public class GameHUD : MonoBehaviour
|
|||||||
GUI.Label(new Rect(panelX + 8f, panelY + 32f, panelW - 16f, 24f), modeFull, modeStyle);
|
GUI.Label(new Rect(panelX + 8f, panelY + 32f, panelW - 16f, 24f), modeFull, modeStyle);
|
||||||
|
|
||||||
// ── Top-right: Players alive ──────────────────────────────────────
|
// ── Top-right: Players alive ──────────────────────────────────────
|
||||||
int alive = nm?.GetLocalPlayerState() != null
|
|
||||||
? (_room_playersAlive > 0 ? _room_playersAlive : 1)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
if (nm != null)
|
|
||||||
{
|
|
||||||
// read from room state if accessible
|
|
||||||
}
|
|
||||||
|
|
||||||
float prX = Screen.width - 180f;
|
float prX = Screen.width - 180f;
|
||||||
GUI.color = new Color(0.08f, 0.08f, 0.12f, 0.85f);
|
GUI.color = new Color(0.08f, 0.08f, 0.12f, 0.85f);
|
||||||
GUI.DrawTexture(new Rect(prX, panelY, 168f, panelH), _bgTex);
|
GUI.DrawTexture(new Rect(prX, panelY, 168f, panelH), _bgTex);
|
||||||
@@ -212,105 +203,15 @@ public class GameHUD : MonoBehaviour
|
|||||||
$"{mins:00}:{secs:00}", timerStyle);
|
$"{mins:00}:{secs:00}", timerStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Race: checkpoint progress (bottom center) ─────────────────────
|
|
||||||
if (_gameMode == "race" && (_phase == "playing" || _localRaceActive))
|
|
||||||
{
|
|
||||||
float bw = 300f;
|
|
||||||
float bx = (Screen.width - bw) / 2f;
|
|
||||||
float by = Screen.height - 60f;
|
|
||||||
|
|
||||||
GUI.color = new Color(0.08f, 0.08f, 0.12f, 0.85f);
|
|
||||||
GUI.DrawTexture(new Rect(bx - 8f, by - 8f, bw + 16f, 36f), _bgTex);
|
|
||||||
GUI.color = Color.white;
|
|
||||||
|
|
||||||
// Background bar
|
|
||||||
GUI.color = new Color(0.2f, 0.2f, 0.28f, 1f);
|
|
||||||
GUI.DrawTexture(new Rect(bx, by, bw, 20f), _barBgTex);
|
|
||||||
|
|
||||||
// Fill
|
|
||||||
float fill = _checkpointsTotal > 0 ? (float)_checkpointsCurrent / _checkpointsTotal : 0f;
|
|
||||||
GUI.color = new Color(0.3f, 1f, 0.5f, 1f);
|
|
||||||
GUI.DrawTexture(new Rect(bx, by, bw * fill, 20f), _barFillTex);
|
|
||||||
GUI.color = Color.white;
|
|
||||||
|
|
||||||
var cpStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 11 };
|
|
||||||
cpStyle.normal.textColor = Color.white;
|
|
||||||
GUI.Label(new Rect(bx, by, bw, 20f),
|
|
||||||
$"Checkpoint {_checkpointsCurrent} / {_checkpointsTotal}", cpStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Teams: score display (bottom center) ──────────────────────────
|
|
||||||
if (_gameMode == "teams" && _phase == "playing")
|
|
||||||
{
|
|
||||||
float tw = 260f;
|
|
||||||
float tx = (Screen.width - tw) / 2f;
|
|
||||||
float ty = Screen.height - 60f;
|
|
||||||
|
|
||||||
GUI.color = new Color(0.08f, 0.08f, 0.12f, 0.85f);
|
|
||||||
GUI.DrawTexture(new Rect(tx - 8f, ty - 8f, tw + 16f, 36f), _bgTex);
|
|
||||||
GUI.color = Color.white;
|
|
||||||
|
|
||||||
var teamStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 20, fontStyle = FontStyle.Bold };
|
|
||||||
|
|
||||||
// Red team score
|
|
||||||
teamStyle.normal.textColor = new Color(1f, 0.3f, 0.3f);
|
|
||||||
GUI.Label(new Rect(tx, ty - 2f, tw * 0.4f, 28f), $"{_cachedScoreRed}", teamStyle);
|
|
||||||
|
|
||||||
// Separator
|
|
||||||
var sepStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 16 };
|
|
||||||
sepStyle.normal.textColor = new Color(0.5f, 0.5f, 0.6f);
|
|
||||||
GUI.Label(new Rect(tx + tw * 0.4f, ty - 2f, tw * 0.2f, 28f), "vs", sepStyle);
|
|
||||||
|
|
||||||
// Blue team score
|
|
||||||
teamStyle.normal.textColor = new Color(0.3f, 0.6f, 1f);
|
|
||||||
GUI.Label(new Rect(tx + tw * 0.6f, ty - 2f, tw * 0.4f, 28f), $"{_cachedScoreBlue}", teamStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Survival: death zone warning ──────────────────────────────────
|
|
||||||
if (_gameMode == "survival" && _phase == "playing" && _deathZoneWarning > 0.01f)
|
|
||||||
{
|
|
||||||
GUI.color = new Color(1f, 0.3f, 0.1f, _deathZoneWarning * 0.4f);
|
|
||||||
GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), _bgTex);
|
|
||||||
GUI.color = Color.white;
|
|
||||||
|
|
||||||
var warnStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 20, fontStyle = FontStyle.Bold };
|
|
||||||
warnStyle.normal.textColor = new Color(1f, 0.4f, 0.2f, _deathZoneWarning);
|
|
||||||
GUI.Label(new Rect(0, Screen.height * 0.8f, Screen.width, 36f), "⚠ ZONE DE MORT MONTE !", warnStyle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static accessors for cross-script use
|
// Static accessors for cross-script use
|
||||||
public static GameHUD Instance { get; private set; }
|
public static GameHUD Instance { get; private set; }
|
||||||
public static int TotalCheckpoints { get; set; } = 5;
|
|
||||||
|
|
||||||
// Cached values updated from NetworkManager state polling
|
// Cached values updated from NetworkManager state polling
|
||||||
private int _cachedPlayersAlive = 0;
|
private int _cachedPlayersAlive = 0;
|
||||||
private int _cachedScoreRed = 0;
|
|
||||||
private int _cachedScoreBlue = 0;
|
|
||||||
private float _deathZoneWarning = 0f;
|
|
||||||
private int _room_playersAlive = 0;
|
|
||||||
|
|
||||||
void LateUpdate()
|
|
||||||
{
|
|
||||||
// Poll NetworkManager for display values (avoids tight coupling via events for display-only data)
|
|
||||||
if (NetworkManager.Instance == null || !NetworkManager.Instance.IsConnected) return;
|
|
||||||
|
|
||||||
// Survival: check death zone proximity
|
|
||||||
if (_gameMode == "survival")
|
|
||||||
{
|
|
||||||
var localState = NetworkManager.Instance.GetLocalPlayerState();
|
|
||||||
if (localState != null)
|
|
||||||
{
|
|
||||||
// deathZoneY is synced via NetworkState — we read via a static accessor pattern
|
|
||||||
// For now, warn when player Y is within 5 units above death zone
|
|
||||||
// (actual deathZoneY is not directly accessible here without extra plumbing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called by DeathZone.cs to update the warning
|
|
||||||
public void SetDeathZoneWarning(float intensity) => _deathZoneWarning = intensity;
|
|
||||||
public void SetTeamScores(int red, int blue) { _cachedScoreRed = red; _cachedScoreBlue = blue; }
|
|
||||||
public void SetPlayersAlive(int count) => _cachedPlayersAlive = count;
|
public void SetPlayersAlive(int count) => _cachedPlayersAlive = count;
|
||||||
|
|
||||||
private static void EnsureTextures()
|
private static void EnsureTextures()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 8a0c9218a650547d98138cd835033977
|
|
||||||
folderAsset: yes
|
|
||||||
timeCreated: 1484670163
|
|
||||||
licenseType: Store
|
|
||||||
DefaultImporter:
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
Binary file not shown.
@@ -1,134 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 727a75301c3d24613a3ebcec4a24c2c8
|
|
||||||
TextureImporter:
|
|
||||||
internalIDToNameTable: []
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 11
|
|
||||||
mipmaps:
|
|
||||||
mipMapMode: 0
|
|
||||||
enableMipMap: 0
|
|
||||||
sRGBTexture: 1
|
|
||||||
linearTexture: 0
|
|
||||||
fadeOut: 0
|
|
||||||
borderMipMap: 0
|
|
||||||
mipMapsPreserveCoverage: 0
|
|
||||||
alphaTestReferenceValue: 0.5
|
|
||||||
mipMapFadeDistanceStart: 1
|
|
||||||
mipMapFadeDistanceEnd: 3
|
|
||||||
bumpmap:
|
|
||||||
convertToNormalMap: 0
|
|
||||||
externalNormalMap: 0
|
|
||||||
heightScale: 0.25
|
|
||||||
normalMapFilter: 0
|
|
||||||
isReadable: 0
|
|
||||||
streamingMipmaps: 0
|
|
||||||
streamingMipmapsPriority: 0
|
|
||||||
vTOnly: 0
|
|
||||||
ignoreMasterTextureLimit: 0
|
|
||||||
grayScaleToAlpha: 0
|
|
||||||
generateCubemap: 6
|
|
||||||
cubemapConvolution: 0
|
|
||||||
seamlessCubemap: 0
|
|
||||||
textureFormat: 1
|
|
||||||
maxTextureSize: 2048
|
|
||||||
textureSettings:
|
|
||||||
serializedVersion: 2
|
|
||||||
filterMode: 0
|
|
||||||
aniso: 1
|
|
||||||
mipBias: 0
|
|
||||||
wrapU: 1
|
|
||||||
wrapV: 1
|
|
||||||
wrapW: 0
|
|
||||||
nPOTScale: 0
|
|
||||||
lightmap: 0
|
|
||||||
compressionQuality: 50
|
|
||||||
spriteMode: 0
|
|
||||||
spriteExtrude: 1
|
|
||||||
spriteMeshType: 1
|
|
||||||
alignment: 0
|
|
||||||
spritePivot: {x: 0.5, y: 0.5}
|
|
||||||
spritePixelsToUnits: 100
|
|
||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
|
||||||
alphaUsage: 1
|
|
||||||
alphaIsTransparency: 1
|
|
||||||
spriteTessellationDetail: -1
|
|
||||||
textureType: 2
|
|
||||||
textureShape: 1
|
|
||||||
singleChannelComponent: 0
|
|
||||||
flipbookRows: 1
|
|
||||||
flipbookColumns: 1
|
|
||||||
maxTextureSizeSet: 0
|
|
||||||
compressionQualitySet: 0
|
|
||||||
textureFormatSet: 0
|
|
||||||
ignorePngGamma: 0
|
|
||||||
applyGammaDecoding: 0
|
|
||||||
platformSettings:
|
|
||||||
- serializedVersion: 3
|
|
||||||
buildTarget: DefaultTexturePlatform
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 0
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
- serializedVersion: 3
|
|
||||||
buildTarget: Standalone
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
- serializedVersion: 3
|
|
||||||
buildTarget: Android
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
- serializedVersion: 3
|
|
||||||
buildTarget: iPhone
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
spriteSheet:
|
|
||||||
serializedVersion: 2
|
|
||||||
sprites: []
|
|
||||||
outline: []
|
|
||||||
physicsShape: []
|
|
||||||
bones: []
|
|
||||||
spriteID:
|
|
||||||
internalID: 0
|
|
||||||
vertices: []
|
|
||||||
indices:
|
|
||||||
edges: []
|
|
||||||
weights: []
|
|
||||||
secondaryTextures: []
|
|
||||||
nameFileIdTable: {}
|
|
||||||
spritePackingTag:
|
|
||||||
pSDRemoveMatte: 0
|
|
||||||
pSDShowRemoveMatteOption: 0
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,654 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!114 &1
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12004, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_PixelRect:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 45
|
|
||||||
width: 1666
|
|
||||||
height: 958
|
|
||||||
m_ShowMode: 4
|
|
||||||
m_Title:
|
|
||||||
m_RootView: {fileID: 6}
|
|
||||||
m_MinSize: {x: 950, y: 542}
|
|
||||||
m_MaxSize: {x: 10000, y: 10000}
|
|
||||||
--- !u!114 &2
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children: []
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 466
|
|
||||||
width: 290
|
|
||||||
height: 442
|
|
||||||
m_MinSize: {x: 234, y: 271}
|
|
||||||
m_MaxSize: {x: 10004, y: 10021}
|
|
||||||
m_ActualView: {fileID: 14}
|
|
||||||
m_Panes:
|
|
||||||
- {fileID: 14}
|
|
||||||
m_Selected: 0
|
|
||||||
m_LastSelected: 0
|
|
||||||
--- !u!114 &3
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 4}
|
|
||||||
- {fileID: 2}
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 973
|
|
||||||
y: 0
|
|
||||||
width: 290
|
|
||||||
height: 908
|
|
||||||
m_MinSize: {x: 234, y: 492}
|
|
||||||
m_MaxSize: {x: 10004, y: 14042}
|
|
||||||
vertical: 1
|
|
||||||
controlID: 226
|
|
||||||
--- !u!114 &4
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children: []
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: 290
|
|
||||||
height: 466
|
|
||||||
m_MinSize: {x: 204, y: 221}
|
|
||||||
m_MaxSize: {x: 4004, y: 4021}
|
|
||||||
m_ActualView: {fileID: 17}
|
|
||||||
m_Panes:
|
|
||||||
- {fileID: 17}
|
|
||||||
m_Selected: 0
|
|
||||||
m_LastSelected: 0
|
|
||||||
--- !u!114 &5
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children: []
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 466
|
|
||||||
width: 973
|
|
||||||
height: 442
|
|
||||||
m_MinSize: {x: 202, y: 221}
|
|
||||||
m_MaxSize: {x: 4002, y: 4021}
|
|
||||||
m_ActualView: {fileID: 15}
|
|
||||||
m_Panes:
|
|
||||||
- {fileID: 15}
|
|
||||||
m_Selected: 0
|
|
||||||
m_LastSelected: 0
|
|
||||||
--- !u!114 &6
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12008, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 7}
|
|
||||||
- {fileID: 8}
|
|
||||||
- {fileID: 9}
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: 1666
|
|
||||||
height: 958
|
|
||||||
m_MinSize: {x: 950, y: 542}
|
|
||||||
m_MaxSize: {x: 10000, y: 10000}
|
|
||||||
--- !u!114 &7
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12011, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children: []
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: 1666
|
|
||||||
height: 30
|
|
||||||
m_MinSize: {x: 0, y: 0}
|
|
||||||
m_MaxSize: {x: 0, y: 0}
|
|
||||||
m_LastLoadedLayoutName: Tutorial
|
|
||||||
--- !u!114 &8
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 10}
|
|
||||||
- {fileID: 3}
|
|
||||||
- {fileID: 11}
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 30
|
|
||||||
width: 1666
|
|
||||||
height: 908
|
|
||||||
m_MinSize: {x: 713, y: 492}
|
|
||||||
m_MaxSize: {x: 18008, y: 14042}
|
|
||||||
vertical: 0
|
|
||||||
controlID: 74
|
|
||||||
--- !u!114 &9
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12042, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children: []
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 938
|
|
||||||
width: 1666
|
|
||||||
height: 20
|
|
||||||
m_MinSize: {x: 0, y: 0}
|
|
||||||
m_MaxSize: {x: 0, y: 0}
|
|
||||||
--- !u!114 &10
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 12}
|
|
||||||
- {fileID: 5}
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: 973
|
|
||||||
height: 908
|
|
||||||
m_MinSize: {x: 202, y: 442}
|
|
||||||
m_MaxSize: {x: 4002, y: 8042}
|
|
||||||
vertical: 1
|
|
||||||
controlID: 75
|
|
||||||
--- !u!114 &11
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children: []
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 1263
|
|
||||||
y: 0
|
|
||||||
width: 403
|
|
||||||
height: 908
|
|
||||||
m_MinSize: {x: 277, y: 71}
|
|
||||||
m_MaxSize: {x: 4002, y: 4021}
|
|
||||||
m_ActualView: {fileID: 13}
|
|
||||||
m_Panes:
|
|
||||||
- {fileID: 13}
|
|
||||||
m_Selected: 0
|
|
||||||
m_LastSelected: 0
|
|
||||||
--- !u!114 &12
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Children: []
|
|
||||||
m_Position:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: 973
|
|
||||||
height: 466
|
|
||||||
m_MinSize: {x: 202, y: 221}
|
|
||||||
m_MaxSize: {x: 4002, y: 4021}
|
|
||||||
m_ActualView: {fileID: 16}
|
|
||||||
m_Panes:
|
|
||||||
- {fileID: 16}
|
|
||||||
m_Selected: 0
|
|
||||||
m_LastSelected: 0
|
|
||||||
--- !u!114 &13
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12019, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_AutoRepaintOnSceneChange: 0
|
|
||||||
m_MinSize: {x: 275, y: 50}
|
|
||||||
m_MaxSize: {x: 4000, y: 4000}
|
|
||||||
m_TitleContent:
|
|
||||||
m_Text: Inspector
|
|
||||||
m_Image: {fileID: -6905738622615590433, guid: 0000000000000000d000000000000000,
|
|
||||||
type: 0}
|
|
||||||
m_Tooltip:
|
|
||||||
m_DepthBufferBits: 0
|
|
||||||
m_Pos:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 2
|
|
||||||
y: 19
|
|
||||||
width: 401
|
|
||||||
height: 887
|
|
||||||
m_ScrollPosition: {x: 0, y: 0}
|
|
||||||
m_InspectorMode: 0
|
|
||||||
m_PreviewResizer:
|
|
||||||
m_CachedPref: -160
|
|
||||||
m_ControlHash: -371814159
|
|
||||||
m_PrefName: Preview_InspectorPreview
|
|
||||||
m_PreviewWindow: {fileID: 0}
|
|
||||||
--- !u!114 &14
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12014, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_AutoRepaintOnSceneChange: 0
|
|
||||||
m_MinSize: {x: 230, y: 250}
|
|
||||||
m_MaxSize: {x: 10000, y: 10000}
|
|
||||||
m_TitleContent:
|
|
||||||
m_Text: Project
|
|
||||||
m_Image: {fileID: -7501376956915960154, guid: 0000000000000000d000000000000000,
|
|
||||||
type: 0}
|
|
||||||
m_Tooltip:
|
|
||||||
m_DepthBufferBits: 0
|
|
||||||
m_Pos:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 2
|
|
||||||
y: 19
|
|
||||||
width: 286
|
|
||||||
height: 421
|
|
||||||
m_SearchFilter:
|
|
||||||
m_NameFilter:
|
|
||||||
m_ClassNames: []
|
|
||||||
m_AssetLabels: []
|
|
||||||
m_AssetBundleNames: []
|
|
||||||
m_VersionControlStates: []
|
|
||||||
m_ReferencingInstanceIDs:
|
|
||||||
m_ScenePaths: []
|
|
||||||
m_ShowAllHits: 0
|
|
||||||
m_SearchArea: 0
|
|
||||||
m_Folders:
|
|
||||||
- Assets
|
|
||||||
m_ViewMode: 0
|
|
||||||
m_StartGridSize: 64
|
|
||||||
m_LastFolders:
|
|
||||||
- Assets
|
|
||||||
m_LastFoldersGridSize: -1
|
|
||||||
m_LastProjectPath: /Users/danielbrauer/Unity Projects/New Unity Project 47
|
|
||||||
m_IsLocked: 0
|
|
||||||
m_FolderTreeState:
|
|
||||||
scrollPos: {x: 0, y: 0}
|
|
||||||
m_SelectedIDs: ee240000
|
|
||||||
m_LastClickedID: 9454
|
|
||||||
m_ExpandedIDs: ee24000000ca9a3bffffff7f
|
|
||||||
m_RenameOverlay:
|
|
||||||
m_UserAcceptedRename: 0
|
|
||||||
m_Name:
|
|
||||||
m_OriginalName:
|
|
||||||
m_EditFieldRect:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: 0
|
|
||||||
height: 0
|
|
||||||
m_UserData: 0
|
|
||||||
m_IsWaitingForDelay: 0
|
|
||||||
m_IsRenaming: 0
|
|
||||||
m_OriginalEventType: 11
|
|
||||||
m_IsRenamingFilename: 1
|
|
||||||
m_ClientGUIView: {fileID: 0}
|
|
||||||
m_SearchString:
|
|
||||||
m_CreateAssetUtility:
|
|
||||||
m_EndAction: {fileID: 0}
|
|
||||||
m_InstanceID: 0
|
|
||||||
m_Path:
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_ResourceFile:
|
|
||||||
m_AssetTreeState:
|
|
||||||
scrollPos: {x: 0, y: 0}
|
|
||||||
m_SelectedIDs: 68fbffff
|
|
||||||
m_LastClickedID: 0
|
|
||||||
m_ExpandedIDs: ee240000
|
|
||||||
m_RenameOverlay:
|
|
||||||
m_UserAcceptedRename: 0
|
|
||||||
m_Name:
|
|
||||||
m_OriginalName:
|
|
||||||
m_EditFieldRect:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: 0
|
|
||||||
height: 0
|
|
||||||
m_UserData: 0
|
|
||||||
m_IsWaitingForDelay: 0
|
|
||||||
m_IsRenaming: 0
|
|
||||||
m_OriginalEventType: 11
|
|
||||||
m_IsRenamingFilename: 1
|
|
||||||
m_ClientGUIView: {fileID: 0}
|
|
||||||
m_SearchString:
|
|
||||||
m_CreateAssetUtility:
|
|
||||||
m_EndAction: {fileID: 0}
|
|
||||||
m_InstanceID: 0
|
|
||||||
m_Path:
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_ResourceFile:
|
|
||||||
m_ListAreaState:
|
|
||||||
m_SelectedInstanceIDs: 68fbffff
|
|
||||||
m_LastClickedInstanceID: -1176
|
|
||||||
m_HadKeyboardFocusLastEvent: 0
|
|
||||||
m_ExpandedInstanceIDs: c6230000
|
|
||||||
m_RenameOverlay:
|
|
||||||
m_UserAcceptedRename: 0
|
|
||||||
m_Name:
|
|
||||||
m_OriginalName:
|
|
||||||
m_EditFieldRect:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: 0
|
|
||||||
height: 0
|
|
||||||
m_UserData: 0
|
|
||||||
m_IsWaitingForDelay: 0
|
|
||||||
m_IsRenaming: 0
|
|
||||||
m_OriginalEventType: 11
|
|
||||||
m_IsRenamingFilename: 1
|
|
||||||
m_ClientGUIView: {fileID: 0}
|
|
||||||
m_CreateAssetUtility:
|
|
||||||
m_EndAction: {fileID: 0}
|
|
||||||
m_InstanceID: 0
|
|
||||||
m_Path:
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_ResourceFile:
|
|
||||||
m_NewAssetIndexInList: -1
|
|
||||||
m_ScrollPosition: {x: 0, y: 0}
|
|
||||||
m_GridSize: 64
|
|
||||||
m_DirectoriesAreaWidth: 110
|
|
||||||
--- !u!114 &15
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12015, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_AutoRepaintOnSceneChange: 1
|
|
||||||
m_MinSize: {x: 200, y: 200}
|
|
||||||
m_MaxSize: {x: 4000, y: 4000}
|
|
||||||
m_TitleContent:
|
|
||||||
m_Text: Game
|
|
||||||
m_Image: {fileID: -2087823869225018852, guid: 0000000000000000d000000000000000,
|
|
||||||
type: 0}
|
|
||||||
m_Tooltip:
|
|
||||||
m_DepthBufferBits: 32
|
|
||||||
m_Pos:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 19
|
|
||||||
width: 971
|
|
||||||
height: 421
|
|
||||||
m_MaximizeOnPlay: 0
|
|
||||||
m_Gizmos: 0
|
|
||||||
m_Stats: 0
|
|
||||||
m_SelectedSizes: 00000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
||||||
m_TargetDisplay: 0
|
|
||||||
m_ZoomArea:
|
|
||||||
m_HRangeLocked: 0
|
|
||||||
m_VRangeLocked: 0
|
|
||||||
m_HBaseRangeMin: -242.75
|
|
||||||
m_HBaseRangeMax: 242.75
|
|
||||||
m_VBaseRangeMin: -101
|
|
||||||
m_VBaseRangeMax: 101
|
|
||||||
m_HAllowExceedBaseRangeMin: 1
|
|
||||||
m_HAllowExceedBaseRangeMax: 1
|
|
||||||
m_VAllowExceedBaseRangeMin: 1
|
|
||||||
m_VAllowExceedBaseRangeMax: 1
|
|
||||||
m_ScaleWithWindow: 0
|
|
||||||
m_HSlider: 0
|
|
||||||
m_VSlider: 0
|
|
||||||
m_IgnoreScrollWheelUntilClicked: 0
|
|
||||||
m_EnableMouseInput: 1
|
|
||||||
m_EnableSliderZoom: 0
|
|
||||||
m_UniformScale: 1
|
|
||||||
m_UpDirection: 1
|
|
||||||
m_DrawArea:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 17
|
|
||||||
width: 971
|
|
||||||
height: 404
|
|
||||||
m_Scale: {x: 2, y: 2}
|
|
||||||
m_Translation: {x: 485.5, y: 202}
|
|
||||||
m_MarginLeft: 0
|
|
||||||
m_MarginRight: 0
|
|
||||||
m_MarginTop: 0
|
|
||||||
m_MarginBottom: 0
|
|
||||||
m_LastShownAreaInsideMargins:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: -242.75
|
|
||||||
y: -101
|
|
||||||
width: 485.5
|
|
||||||
height: 202
|
|
||||||
m_MinimalGUI: 1
|
|
||||||
m_defaultScale: 2
|
|
||||||
m_TargetTexture: {fileID: 0}
|
|
||||||
m_CurrentColorSpace: 0
|
|
||||||
m_LastWindowPixelSize: {x: 1942, y: 842}
|
|
||||||
m_ClearInEditMode: 1
|
|
||||||
m_NoCameraWarning: 1
|
|
||||||
m_LowResolutionForAspectRatios: 01000000000100000100
|
|
||||||
--- !u!114 &16
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12013, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_AutoRepaintOnSceneChange: 1
|
|
||||||
m_MinSize: {x: 200, y: 200}
|
|
||||||
m_MaxSize: {x: 4000, y: 4000}
|
|
||||||
m_TitleContent:
|
|
||||||
m_Text: Scene
|
|
||||||
m_Image: {fileID: 2318424515335265636, guid: 0000000000000000d000000000000000,
|
|
||||||
type: 0}
|
|
||||||
m_Tooltip:
|
|
||||||
m_DepthBufferBits: 32
|
|
||||||
m_Pos:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 19
|
|
||||||
width: 971
|
|
||||||
height: 445
|
|
||||||
m_SceneLighting: 1
|
|
||||||
lastFramingTime: 0
|
|
||||||
m_2DMode: 0
|
|
||||||
m_isRotationLocked: 0
|
|
||||||
m_AudioPlay: 0
|
|
||||||
m_Position:
|
|
||||||
m_Target: {x: 0, y: 0, z: 0}
|
|
||||||
speed: 2
|
|
||||||
m_Value: {x: 0, y: 0, z: 0}
|
|
||||||
m_RenderMode: 0
|
|
||||||
m_ValidateTrueMetals: 0
|
|
||||||
m_SceneViewState:
|
|
||||||
showFog: 1
|
|
||||||
showMaterialUpdate: 0
|
|
||||||
showSkybox: 1
|
|
||||||
showFlares: 1
|
|
||||||
showImageEffects: 1
|
|
||||||
grid:
|
|
||||||
xGrid:
|
|
||||||
m_Target: 0
|
|
||||||
speed: 2
|
|
||||||
m_Value: 0
|
|
||||||
yGrid:
|
|
||||||
m_Target: 1
|
|
||||||
speed: 2
|
|
||||||
m_Value: 1
|
|
||||||
zGrid:
|
|
||||||
m_Target: 0
|
|
||||||
speed: 2
|
|
||||||
m_Value: 0
|
|
||||||
m_Rotation:
|
|
||||||
m_Target: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226}
|
|
||||||
speed: 2
|
|
||||||
m_Value: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226}
|
|
||||||
m_Size:
|
|
||||||
m_Target: 10
|
|
||||||
speed: 2
|
|
||||||
m_Value: 10
|
|
||||||
m_Ortho:
|
|
||||||
m_Target: 0
|
|
||||||
speed: 2
|
|
||||||
m_Value: 0
|
|
||||||
m_LastSceneViewRotation: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
m_LastSceneViewOrtho: 0
|
|
||||||
m_ReplacementShader: {fileID: 0}
|
|
||||||
m_ReplacementString:
|
|
||||||
m_LastLockedObject: {fileID: 0}
|
|
||||||
m_ViewIsLockedToObject: 0
|
|
||||||
--- !u!114 &17
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 52
|
|
||||||
m_PrefabParentObject: {fileID: 0}
|
|
||||||
m_PrefabInternal: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 1
|
|
||||||
m_Script: {fileID: 12061, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_AutoRepaintOnSceneChange: 0
|
|
||||||
m_MinSize: {x: 200, y: 200}
|
|
||||||
m_MaxSize: {x: 4000, y: 4000}
|
|
||||||
m_TitleContent:
|
|
||||||
m_Text: Hierarchy
|
|
||||||
m_Image: {fileID: -590624980919486359, guid: 0000000000000000d000000000000000,
|
|
||||||
type: 0}
|
|
||||||
m_Tooltip:
|
|
||||||
m_DepthBufferBits: 0
|
|
||||||
m_Pos:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 2
|
|
||||||
y: 19
|
|
||||||
width: 286
|
|
||||||
height: 445
|
|
||||||
m_TreeViewState:
|
|
||||||
scrollPos: {x: 0, y: 0}
|
|
||||||
m_SelectedIDs: 68fbffff
|
|
||||||
m_LastClickedID: -1176
|
|
||||||
m_ExpandedIDs: 7efbffff00000000
|
|
||||||
m_RenameOverlay:
|
|
||||||
m_UserAcceptedRename: 0
|
|
||||||
m_Name:
|
|
||||||
m_OriginalName:
|
|
||||||
m_EditFieldRect:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: 0
|
|
||||||
height: 0
|
|
||||||
m_UserData: 0
|
|
||||||
m_IsWaitingForDelay: 0
|
|
||||||
m_IsRenaming: 0
|
|
||||||
m_OriginalEventType: 11
|
|
||||||
m_IsRenamingFilename: 0
|
|
||||||
m_ClientGUIView: {fileID: 0}
|
|
||||||
m_SearchString:
|
|
||||||
m_ExpandedScenes:
|
|
||||||
-
|
|
||||||
m_CurrenRootInstanceID: 0
|
|
||||||
m_Locked: 0
|
|
||||||
m_CurrentSortingName: TransformSorting
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: eabc9546105bf4accac1fd62a63e88e6
|
|
||||||
timeCreated: 1487337779
|
|
||||||
licenseType: Store
|
|
||||||
DefaultImporter:
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 5a9bcd70e6a4b4b05badaa72e827d8e0
|
|
||||||
folderAsset: yes
|
|
||||||
timeCreated: 1475835190
|
|
||||||
licenseType: Store
|
|
||||||
DefaultImporter:
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 3ad9b87dffba344c89909c6d1b1c17e1
|
|
||||||
folderAsset: yes
|
|
||||||
timeCreated: 1475593892
|
|
||||||
licenseType: Store
|
|
||||||
DefaultImporter:
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
[CustomEditor(typeof(Readme))]
|
|
||||||
[InitializeOnLoad]
|
|
||||||
public class ReadmeEditor : Editor
|
|
||||||
{
|
|
||||||
static string s_ShowedReadmeSessionStateName = "ReadmeEditor.showedReadme";
|
|
||||||
|
|
||||||
static string s_ReadmeSourceDirectory = "Assets/TutorialInfo";
|
|
||||||
|
|
||||||
const float k_Space = 16f;
|
|
||||||
|
|
||||||
static ReadmeEditor()
|
|
||||||
{
|
|
||||||
EditorApplication.delayCall += SelectReadmeAutomatically;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void RemoveTutorial()
|
|
||||||
{
|
|
||||||
if (EditorUtility.DisplayDialog("Remove Readme Assets",
|
|
||||||
|
|
||||||
$"All contents under {s_ReadmeSourceDirectory} will be removed, are you sure you want to proceed?",
|
|
||||||
"Proceed",
|
|
||||||
"Cancel"))
|
|
||||||
{
|
|
||||||
if (Directory.Exists(s_ReadmeSourceDirectory))
|
|
||||||
{
|
|
||||||
FileUtil.DeleteFileOrDirectory(s_ReadmeSourceDirectory);
|
|
||||||
FileUtil.DeleteFileOrDirectory(s_ReadmeSourceDirectory + ".meta");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.Log($"Could not find the Readme folder at {s_ReadmeSourceDirectory}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var readmeAsset = SelectReadme();
|
|
||||||
if (readmeAsset != null)
|
|
||||||
{
|
|
||||||
var path = AssetDatabase.GetAssetPath(readmeAsset);
|
|
||||||
FileUtil.DeleteFileOrDirectory(path + ".meta");
|
|
||||||
FileUtil.DeleteFileOrDirectory(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetDatabase.Refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void SelectReadmeAutomatically()
|
|
||||||
{
|
|
||||||
if (!SessionState.GetBool(s_ShowedReadmeSessionStateName, false))
|
|
||||||
{
|
|
||||||
var readme = SelectReadme();
|
|
||||||
SessionState.SetBool(s_ShowedReadmeSessionStateName, true);
|
|
||||||
|
|
||||||
if (readme && !readme.loadedLayout)
|
|
||||||
{
|
|
||||||
LoadLayout();
|
|
||||||
readme.loadedLayout = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void LoadLayout()
|
|
||||||
{
|
|
||||||
var assembly = typeof(EditorApplication).Assembly;
|
|
||||||
var windowLayoutType = assembly.GetType("UnityEditor.WindowLayout", true);
|
|
||||||
var method = windowLayoutType.GetMethod("LoadWindowLayout", BindingFlags.Public | BindingFlags.Static);
|
|
||||||
method.Invoke(null, new object[] { Path.Combine(Application.dataPath, "TutorialInfo/Layout.wlt"), false });
|
|
||||||
}
|
|
||||||
|
|
||||||
static Readme SelectReadme()
|
|
||||||
{
|
|
||||||
var ids = AssetDatabase.FindAssets("Readme t:Readme");
|
|
||||||
if (ids.Length == 1)
|
|
||||||
{
|
|
||||||
var readmeObject = AssetDatabase.LoadMainAssetAtPath(AssetDatabase.GUIDToAssetPath(ids[0]));
|
|
||||||
|
|
||||||
Selection.objects = new UnityEngine.Object[] { readmeObject };
|
|
||||||
|
|
||||||
return (Readme)readmeObject;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.Log("Couldn't find a readme");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnHeaderGUI()
|
|
||||||
{
|
|
||||||
var readme = (Readme)target;
|
|
||||||
Init();
|
|
||||||
|
|
||||||
var iconWidth = Mathf.Min(EditorGUIUtility.currentViewWidth / 3f - 20f, 128f);
|
|
||||||
|
|
||||||
GUILayout.BeginHorizontal("In BigTitle");
|
|
||||||
{
|
|
||||||
if (readme.icon != null)
|
|
||||||
{
|
|
||||||
GUILayout.Space(k_Space);
|
|
||||||
GUILayout.Label(readme.icon, GUILayout.Width(iconWidth), GUILayout.Height(iconWidth));
|
|
||||||
}
|
|
||||||
GUILayout.Space(k_Space);
|
|
||||||
GUILayout.BeginVertical();
|
|
||||||
{
|
|
||||||
|
|
||||||
GUILayout.FlexibleSpace();
|
|
||||||
GUILayout.Label(readme.title, TitleStyle);
|
|
||||||
GUILayout.FlexibleSpace();
|
|
||||||
}
|
|
||||||
GUILayout.EndVertical();
|
|
||||||
GUILayout.FlexibleSpace();
|
|
||||||
}
|
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnInspectorGUI()
|
|
||||||
{
|
|
||||||
var readme = (Readme)target;
|
|
||||||
Init();
|
|
||||||
|
|
||||||
foreach (var section in readme.sections)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(section.heading))
|
|
||||||
{
|
|
||||||
GUILayout.Label(section.heading, HeadingStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(section.text))
|
|
||||||
{
|
|
||||||
GUILayout.Label(section.text, BodyStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(section.linkText))
|
|
||||||
{
|
|
||||||
if (LinkLabel(new GUIContent(section.linkText)))
|
|
||||||
{
|
|
||||||
Application.OpenURL(section.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GUILayout.Space(k_Space);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GUILayout.Button("Remove Readme Assets", ButtonStyle))
|
|
||||||
{
|
|
||||||
RemoveTutorial();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool m_Initialized;
|
|
||||||
|
|
||||||
GUIStyle LinkStyle
|
|
||||||
{
|
|
||||||
get { return m_LinkStyle; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
GUIStyle m_LinkStyle;
|
|
||||||
|
|
||||||
GUIStyle TitleStyle
|
|
||||||
{
|
|
||||||
get { return m_TitleStyle; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
GUIStyle m_TitleStyle;
|
|
||||||
|
|
||||||
GUIStyle HeadingStyle
|
|
||||||
{
|
|
||||||
get { return m_HeadingStyle; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
GUIStyle m_HeadingStyle;
|
|
||||||
|
|
||||||
GUIStyle BodyStyle
|
|
||||||
{
|
|
||||||
get { return m_BodyStyle; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
GUIStyle m_BodyStyle;
|
|
||||||
|
|
||||||
GUIStyle ButtonStyle
|
|
||||||
{
|
|
||||||
get { return m_ButtonStyle; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
GUIStyle m_ButtonStyle;
|
|
||||||
|
|
||||||
void Init()
|
|
||||||
{
|
|
||||||
if (m_Initialized)
|
|
||||||
return;
|
|
||||||
m_BodyStyle = new GUIStyle(EditorStyles.label);
|
|
||||||
m_BodyStyle.wordWrap = true;
|
|
||||||
m_BodyStyle.fontSize = 14;
|
|
||||||
m_BodyStyle.richText = true;
|
|
||||||
|
|
||||||
m_TitleStyle = new GUIStyle(m_BodyStyle);
|
|
||||||
m_TitleStyle.fontSize = 26;
|
|
||||||
|
|
||||||
m_HeadingStyle = new GUIStyle(m_BodyStyle);
|
|
||||||
m_HeadingStyle.fontStyle = FontStyle.Bold;
|
|
||||||
m_HeadingStyle.fontSize = 18;
|
|
||||||
|
|
||||||
m_LinkStyle = new GUIStyle(m_BodyStyle);
|
|
||||||
m_LinkStyle.wordWrap = false;
|
|
||||||
|
|
||||||
// Match selection color which works nicely for both light and dark skins
|
|
||||||
m_LinkStyle.normal.textColor = new Color(0x00 / 255f, 0x78 / 255f, 0xDA / 255f, 1f);
|
|
||||||
m_LinkStyle.stretchWidth = false;
|
|
||||||
|
|
||||||
m_ButtonStyle = new GUIStyle(EditorStyles.miniButton);
|
|
||||||
m_ButtonStyle.fontStyle = FontStyle.Bold;
|
|
||||||
|
|
||||||
m_Initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool LinkLabel(GUIContent label, params GUILayoutOption[] options)
|
|
||||||
{
|
|
||||||
var position = GUILayoutUtility.GetRect(label, LinkStyle, options);
|
|
||||||
|
|
||||||
Handles.BeginGUI();
|
|
||||||
Handles.color = LinkStyle.normal.textColor;
|
|
||||||
Handles.DrawLine(new Vector3(position.xMin, position.yMax), new Vector3(position.xMax, position.yMax));
|
|
||||||
Handles.color = Color.white;
|
|
||||||
Handles.EndGUI();
|
|
||||||
|
|
||||||
EditorGUIUtility.AddCursorRect(position, MouseCursor.Link);
|
|
||||||
|
|
||||||
return GUI.Button(position, label, LinkStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 476cc7d7cd9874016adc216baab94a0a
|
|
||||||
timeCreated: 1484146680
|
|
||||||
licenseType: Store
|
|
||||||
MonoImporter:
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
public class Readme : ScriptableObject
|
|
||||||
{
|
|
||||||
public Texture2D icon;
|
|
||||||
public string title;
|
|
||||||
public Section[] sections;
|
|
||||||
public bool loadedLayout;
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class Section
|
|
||||||
{
|
|
||||||
public string heading, text, linkText, url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: fcf7219bab7fe46a1ad266029b2fee19
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences:
|
|
||||||
- icon: {instanceID: 0}
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {fileID: 2800000, guid: a186f8a87ca4f4d3aa864638ad5dfb65, type: 3}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
52
rolld_backend/game/src/chat/ChatManager.js
Normal file
52
rolld_backend/game/src/chat/ChatManager.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const DATA_FILE = path.join(__dirname, "../../data/chat.json");
|
||||||
|
const MAX_MESSAGES = 200;
|
||||||
|
|
||||||
|
let _messages = [];
|
||||||
|
let _nextId = 1;
|
||||||
|
|
||||||
|
function _load() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(DATA_FILE)) {
|
||||||
|
const data = JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
|
||||||
|
_messages = data.messages || [];
|
||||||
|
_nextId = data.nextId || (_messages.length + 1);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[Chat] Failed to load chat.json:", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _save() {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true });
|
||||||
|
fs.writeFileSync(DATA_FILE, JSON.stringify({ messages: _messages, nextId: _nextId }, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[Chat] Failed to save chat.json:", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function push(name, text) {
|
||||||
|
if (!name || !text || typeof name !== "string" || typeof text !== "string") return null;
|
||||||
|
name = name.slice(0, 32);
|
||||||
|
text = text.slice(0, 200);
|
||||||
|
if (text.trim().length === 0) return null;
|
||||||
|
|
||||||
|
const msg = { id: _nextId++, timestamp: Date.now(), name, text: text.trim() };
|
||||||
|
_messages.push(msg);
|
||||||
|
if (_messages.length > MAX_MESSAGES) _messages.splice(0, _messages.length - MAX_MESSAGES);
|
||||||
|
_save();
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHistory(since) {
|
||||||
|
const ts = Number(since) || 0;
|
||||||
|
return ts === 0 ? _messages.slice(-50) : _messages.filter((m) => m.timestamp > ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
_load();
|
||||||
|
console.log(`[Chat] Loaded ${_messages.length} message(s)`);
|
||||||
|
|
||||||
|
module.exports = { push, getHistory };
|
||||||
@@ -1,27 +1,115 @@
|
|||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const { Server } = require('@colyseus/core');
|
const { Server, matchMaker } = require('@colyseus/core');
|
||||||
const { WebSocketTransport } = require('@colyseus/ws-transport');
|
const { WebSocketTransport } = require('@colyseus/ws-transport');
|
||||||
const { ArenaRoom } = require('./rooms/ArenaRoom');
|
const { ArenaRoom } = require('./rooms/ArenaRoom');
|
||||||
|
const Stats = require('./stats/StatsManager');
|
||||||
|
const Chat = require('./chat/ChatManager');
|
||||||
|
const { z } = require('zod');
|
||||||
|
|
||||||
const PORT = process.env.PORT || 2567;
|
const PORT = process.env.PORT || 2567;
|
||||||
|
|
||||||
// Colyseus 0.17 – express callback receives the transport's internal Express app
|
const statsUpdateSchema = z.object({
|
||||||
|
name: z.string().min(1).max(32),
|
||||||
|
stats: z.object({
|
||||||
|
totalDistance: z.number().optional(),
|
||||||
|
totalJumps: z.number().optional(),
|
||||||
|
maxSpeed: z.number().optional(),
|
||||||
|
bestRaceTime: z.number().optional(),
|
||||||
|
racesPlayed: z.number().optional(),
|
||||||
|
qualifications: z.number().optional(),
|
||||||
|
eliminations: z.number().optional(),
|
||||||
|
checkpointsTotal: z.number().optional(),
|
||||||
|
bumpsGiven: z.number().optional(),
|
||||||
|
totalPlaytime: z.number().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatSendSchema = z.object({
|
||||||
|
name: z.string().min(1).max(32),
|
||||||
|
text: z.string().min(1).max(200),
|
||||||
|
});
|
||||||
|
|
||||||
|
let _gameServer;
|
||||||
|
|
||||||
const gameServer = new Server({
|
const gameServer = new Server({
|
||||||
transport: new WebSocketTransport(),
|
transport: new WebSocketTransport(),
|
||||||
express: (app) => {
|
express: (app) => {
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
app.use(require('express').json());
|
||||||
|
|
||||||
app.get('/health', (_req, res) => {
|
app.get('/health', (_req, res) => {
|
||||||
res.json({ service: 'game', status: 'ok', timestamp: new Date().toISOString() });
|
res.json({ service: 'game', status: 'ok', timestamp: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/', (_req, res) => {
|
app.get('/', (_req, res) => res.send('🎮 Game server running'));
|
||||||
res.send('🎮 Game server running');
|
|
||||||
|
// ── Stats ────────────────────────────────────────────────────────────
|
||||||
|
app.get('/stats', (_req, res) => {
|
||||||
|
res.json(Stats.getAll());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/stats/leaderboard/:key', (req, res) => {
|
||||||
|
const board = Stats.getLeaderboard(req.params.key);
|
||||||
|
if (!board) return res.status(400).json({ error: 'invalid key' });
|
||||||
|
res.json(board);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/stats/update', (req, res) => {
|
||||||
|
const parsed = statsUpdateSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.warn('[Stats] Bad update request:', JSON.stringify(parsed.error.issues));
|
||||||
|
return res.status(400).json({ error: parsed.error.issues });
|
||||||
|
}
|
||||||
|
const ok = Stats.update(parsed.data.name, parsed.data.stats);
|
||||||
|
console.log(`[Stats] Update for "${parsed.data.name}": ok=${ok}`, JSON.stringify(parsed.data.stats));
|
||||||
|
res.json({ ok });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Rooms ────────────────────────────────────────────────────────────
|
||||||
|
app.get('/rooms', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const rooms = await matchMaker.query({ name: 'arena' });
|
||||||
|
res.json(rooms.map(r => ({
|
||||||
|
roomId: r.roomId,
|
||||||
|
clients: r.clients,
|
||||||
|
maxClients: r.maxClients,
|
||||||
|
metadata: r.metadata || {},
|
||||||
|
})));
|
||||||
|
} catch (_) {
|
||||||
|
res.json([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Chat ─────────────────────────────────────────────────────────────
|
||||||
|
app.get('/chat/history', (req, res) => {
|
||||||
|
res.json(Chat.getHistory(req.query.since));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/chat/send', (req, res) => {
|
||||||
|
const parsed = chatSendSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) return res.status(400).json({ error: parsed.error.issues });
|
||||||
|
const msg = Chat.push(parsed.data.name, parsed.data.text);
|
||||||
|
if (!msg) return res.status(429).json({ error: 'empty or invalid message' });
|
||||||
|
|
||||||
|
// Broadcast to all active Colyseus rooms
|
||||||
|
if (_gameServer) {
|
||||||
|
try {
|
||||||
|
const rooms = _gameServer.matchMaker?.rooms;
|
||||||
|
if (rooms) {
|
||||||
|
for (const room of rooms.values()) {
|
||||||
|
room.broadcast('chat', msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(msg);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define rooms
|
_gameServer = gameServer;
|
||||||
|
|
||||||
gameServer.define('arena', ArenaRoom);
|
gameServer.define('arena', ArenaRoom);
|
||||||
console.log('✅ ArenaRoom registered');
|
console.log('✅ ArenaRoom registered');
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
const { Room } = require("@colyseus/core");
|
const { Room } = require("@colyseus/core");
|
||||||
const { GameState, Player } = require("../schema/GameState");
|
const { GameState, Player } = require("../schema/GameState");
|
||||||
|
const Chat = require("../chat/ChatManager");
|
||||||
|
|
||||||
const ROUND_MODES = ["race", "survival", "teams"];
|
const LOBBY_TIMEOUT = 30;
|
||||||
const LOBBY_TIMEOUT = 30; // seconds before auto-start
|
|
||||||
const COUNTDOWN_DURATION = 3;
|
const COUNTDOWN_DURATION = 3;
|
||||||
const ROUND_END_DURATION = 5;
|
const ROUND_END_DURATION = 5;
|
||||||
const RACE_TIMEOUT = 180; // 3 min
|
|
||||||
const SURVIVAL_START_DELAY = 20; // seconds before deathzone rises
|
const QUALIFY_RATIO = 0.6;
|
||||||
const SURVIVAL_RISE_RATE = 0.3; // units/sec
|
|
||||||
const SURVIVAL_MAX_Y = 15;
|
|
||||||
const TEAMS_DURATION = 90;
|
|
||||||
const QUALIFY_RATIO = 0.6; // top 60% qualify in race
|
|
||||||
|
|
||||||
class ArenaRoom extends Room {
|
class ArenaRoom extends Room {
|
||||||
maxClients = 20;
|
maxClients = 20;
|
||||||
@@ -18,12 +14,10 @@ class ArenaRoom extends Room {
|
|||||||
onCreate(options) {
|
onCreate(options) {
|
||||||
this.setState(new GameState());
|
this.setState(new GameState());
|
||||||
this.setPatchRate(16); // ~62.5 Hz
|
this.setPatchRate(16); // ~62.5 Hz
|
||||||
|
this.setMetadata({ name: options?.roomName || ('Salle #' + this.roomId.substring(0, 6)) });
|
||||||
|
|
||||||
this._phaseTimer = null;
|
this._phaseTimer = null;
|
||||||
this._survivalInterval = null;
|
|
||||||
this._teamInterval = null;
|
|
||||||
this._lobbyTimer = null;
|
this._lobbyTimer = null;
|
||||||
this._inZonePlayers = new Set(); // sessionIds currently in zone
|
|
||||||
|
|
||||||
console.log(`[ArenaRoom] Room ${this.roomId} created`);
|
console.log(`[ArenaRoom] Room ${this.roomId} created`);
|
||||||
|
|
||||||
@@ -54,36 +48,25 @@ class ArenaRoom extends Room {
|
|||||||
this._checkAllReady();
|
this._checkAllReady();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.onMessage("chat", (client, data) => {
|
||||||
|
const player = this.state.players.get(client.sessionId);
|
||||||
|
if (!player || !data.text) return;
|
||||||
|
const msg = Chat.push(player.name, data.text);
|
||||||
|
if (msg) this.broadcast("chat", msg);
|
||||||
|
});
|
||||||
|
|
||||||
this.onMessage("checkpointReached", (client, data) => {
|
this.onMessage("checkpointReached", (client, data) => {
|
||||||
if (this.state.phase !== "playing" || this.state.gameMode !== "race") return;
|
if (this.state.phase !== "playing") return;
|
||||||
const player = this.state.players.get(client.sessionId);
|
const player = this.state.players.get(client.sessionId);
|
||||||
if (!player || player.isEliminated || player.isQualified) return;
|
if (!player || player.isEliminated || player.isQualified) return;
|
||||||
const expected = player.checkpointIndex;
|
const expected = player.checkpointIndex;
|
||||||
if (data.index !== expected) return; // must hit in order
|
if (data.index !== expected) return;
|
||||||
player.checkpointIndex = data.index + 1;
|
player.checkpointIndex = data.index + 1;
|
||||||
// The last checkpoint (index 4 = finish) qualifies the player
|
|
||||||
// CheckpointSystem sends index after increment, so finish = totalCheckpoints
|
|
||||||
const TOTAL_CHECKPOINTS = 5;
|
const TOTAL_CHECKPOINTS = 5;
|
||||||
if (player.checkpointIndex >= TOTAL_CHECKPOINTS) {
|
if (player.checkpointIndex >= TOTAL_CHECKPOINTS) {
|
||||||
this._qualifyPlayer(client.sessionId, "finish");
|
this._qualifyPlayer(client.sessionId, "finish");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onMessage("deathZoneHit", (client) => {
|
|
||||||
if (this.state.phase !== "playing" || this.state.gameMode !== "survival") return;
|
|
||||||
this._eliminatePlayer(client.sessionId, "deathzone");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onMessage("inZone", (client, data) => {
|
|
||||||
if (this.state.phase !== "playing" || this.state.gameMode !== "teams") return;
|
|
||||||
const player = this.state.players.get(client.sessionId);
|
|
||||||
if (!player || player.isEliminated) return;
|
|
||||||
if (data.inZone) {
|
|
||||||
this._inZonePlayers.add(client.sessionId);
|
|
||||||
} else {
|
|
||||||
this._inZonePlayers.delete(client.sessionId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onJoin(client, options) {
|
onJoin(client, options) {
|
||||||
@@ -101,7 +84,6 @@ class ArenaRoom extends Room {
|
|||||||
this.state.players.set(client.sessionId, player);
|
this.state.players.set(client.sessionId, player);
|
||||||
this._updatePlayersAlive();
|
this._updatePlayersAlive();
|
||||||
|
|
||||||
// Auto-start lobby timer on first player
|
|
||||||
if (this.state.players.size === 1 && this.state.phase === "lobby") {
|
if (this.state.players.size === 1 && this.state.phase === "lobby") {
|
||||||
this._startLobbyTimer();
|
this._startLobbyTimer();
|
||||||
}
|
}
|
||||||
@@ -109,7 +91,6 @@ class ArenaRoom extends Room {
|
|||||||
|
|
||||||
onLeave(client, consented) {
|
onLeave(client, consented) {
|
||||||
console.log(`[ArenaRoom] ${client.sessionId} left`);
|
console.log(`[ArenaRoom] ${client.sessionId} left`);
|
||||||
this._inZonePlayers.delete(client.sessionId);
|
|
||||||
this.state.players.delete(client.sessionId);
|
this.state.players.delete(client.sessionId);
|
||||||
this._updatePlayersAlive();
|
this._updatePlayersAlive();
|
||||||
if (this.state.phase === "playing") {
|
if (this.state.phase === "playing") {
|
||||||
@@ -159,29 +140,17 @@ class ArenaRoom extends Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_startPlaying() {
|
_startPlaying() {
|
||||||
const modeIndex = (this.state.roundNumber - 1) % ROUND_MODES.length;
|
this.state.gameMode = "race";
|
||||||
this.state.gameMode = ROUND_MODES[modeIndex];
|
|
||||||
this.state.phase = "playing";
|
this.state.phase = "playing";
|
||||||
this.state.countdown = 0;
|
this.state.countdown = 0;
|
||||||
|
|
||||||
// Reset player state for new round
|
this.state.players.forEach((p) => {
|
||||||
let teamToggle = 0;
|
|
||||||
this.state.players.forEach((p, id) => {
|
|
||||||
p.isEliminated = false;
|
p.isEliminated = false;
|
||||||
p.isQualified = false;
|
p.isQualified = false;
|
||||||
p.isReady = false;
|
p.isReady = false;
|
||||||
p.checkpointIndex = 0;
|
p.checkpointIndex = 0;
|
||||||
if (this.state.gameMode === "teams") {
|
|
||||||
p.team = (teamToggle++ % 2 === 0) ? 1 : 2;
|
|
||||||
} else {
|
|
||||||
p.team = 0;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.state.deathZoneY = -50;
|
|
||||||
this.state.teamScoreRed = 0;
|
|
||||||
this.state.teamScoreBlue = 0;
|
|
||||||
this._inZonePlayers.clear();
|
|
||||||
this._updatePlayersAlive();
|
this._updatePlayersAlive();
|
||||||
|
|
||||||
this.broadcast("roundStart", {
|
this.broadcast("roundStart", {
|
||||||
@@ -190,16 +159,8 @@ class ArenaRoom extends Room {
|
|||||||
totalRounds: this.state.totalRounds,
|
totalRounds: this.state.totalRounds,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[ArenaRoom] Round ${this.state.roundNumber} started (mode: ${this.state.gameMode})`);
|
console.log(`[ArenaRoom] Round ${this.state.roundNumber} started (race)`);
|
||||||
|
|
||||||
if (this.state.gameMode === "race") {
|
|
||||||
this._phaseTimer = setTimeout(() => this._endRaceTimeout(), RACE_TIMEOUT * 1000);
|
|
||||||
} else if (this.state.gameMode === "survival") {
|
|
||||||
this._phaseTimer = setTimeout(() => this._startSurvivalRise(), SURVIVAL_START_DELAY * 1000);
|
|
||||||
} else if (this.state.gameMode === "teams") {
|
|
||||||
this._startTeamsScoring();
|
|
||||||
this._phaseTimer = setTimeout(() => this._endTeamsRound(), TEAMS_DURATION * 1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_endRound() {
|
_endRound() {
|
||||||
@@ -209,7 +170,6 @@ class ArenaRoom extends Room {
|
|||||||
this.broadcast("roundEnd", { round: this.state.roundNumber });
|
this.broadcast("roundEnd", { round: this.state.roundNumber });
|
||||||
console.log(`[ArenaRoom] Round ${this.state.roundNumber} ended`);
|
console.log(`[ArenaRoom] Round ${this.state.roundNumber} ended`);
|
||||||
|
|
||||||
// Check if all rounds done
|
|
||||||
if (this.state.roundNumber >= this.state.totalRounds) {
|
if (this.state.roundNumber >= this.state.totalRounds) {
|
||||||
this._phaseTimer = setTimeout(() => this._endGame(), ROUND_END_DURATION * 1000);
|
this._phaseTimer = setTimeout(() => this._endGame(), ROUND_END_DURATION * 1000);
|
||||||
} else {
|
} else {
|
||||||
@@ -220,13 +180,10 @@ class ArenaRoom extends Room {
|
|||||||
_nextRound() {
|
_nextRound() {
|
||||||
this.state.roundNumber += 1;
|
this.state.roundNumber += 1;
|
||||||
this.state.phase = "lobby";
|
this.state.phase = "lobby";
|
||||||
this.state.playersAlive = 0;
|
|
||||||
this.state.players.forEach((p) => {
|
this.state.players.forEach((p) => {
|
||||||
if (!p.isEliminated) {
|
p.isReady = false;
|
||||||
p.isReady = false;
|
const spawn = this._findSpawnPosition();
|
||||||
const spawn = this._findSpawnPosition();
|
p.x = spawn.x; p.y = spawn.y; p.z = spawn.z;
|
||||||
p.x = spawn.x; p.y = spawn.y; p.z = spawn.z;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
this._updatePlayersAlive();
|
this._updatePlayersAlive();
|
||||||
this._lobbyTimer = null;
|
this._lobbyTimer = null;
|
||||||
@@ -236,7 +193,6 @@ class ArenaRoom extends Room {
|
|||||||
|
|
||||||
_endGame() {
|
_endGame() {
|
||||||
this.state.phase = "gameEnd";
|
this.state.phase = "gameEnd";
|
||||||
// Find winner: last qualified player, or player with most checkpoints
|
|
||||||
let winner = "";
|
let winner = "";
|
||||||
let best = -1;
|
let best = -1;
|
||||||
this.state.players.forEach((p) => {
|
this.state.players.forEach((p) => {
|
||||||
@@ -248,61 +204,6 @@ class ArenaRoom extends Room {
|
|||||||
console.log(`[ArenaRoom] Game over — winner: ${winner}`);
|
console.log(`[ArenaRoom] Game over — winner: ${winner}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Race mode ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_endRaceTimeout() {
|
|
||||||
// Eliminate anyone who hasn't qualified
|
|
||||||
this.state.players.forEach((p, id) => {
|
|
||||||
if (!p.isQualified && !p.isEliminated) {
|
|
||||||
this._eliminatePlayer(id, "timeout");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._endRound();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Survival mode ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_startSurvivalRise() {
|
|
||||||
console.log(`[ArenaRoom] DeathZone starts rising`);
|
|
||||||
this._survivalInterval = setInterval(() => {
|
|
||||||
this.state.deathZoneY += SURVIVAL_RISE_RATE * (16 / 1000);
|
|
||||||
if (this.state.deathZoneY > SURVIVAL_MAX_Y) {
|
|
||||||
this.state.deathZoneY = SURVIVAL_MAX_Y;
|
|
||||||
}
|
|
||||||
}, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Teams mode ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_startTeamsScoring() {
|
|
||||||
this._teamInterval = setInterval(() => {
|
|
||||||
let redInZone = 0;
|
|
||||||
let blueInZone = 0;
|
|
||||||
this._inZonePlayers.forEach((id) => {
|
|
||||||
const p = this.state.players.get(id);
|
|
||||||
if (!p || p.isEliminated) return;
|
|
||||||
if (p.team === 1) redInZone++;
|
|
||||||
else if (p.team === 2) blueInZone++;
|
|
||||||
});
|
|
||||||
if (redInZone > blueInZone) this.state.teamScoreRed = Math.min(this.state.teamScoreRed + 1, 32767);
|
|
||||||
else if (blueInZone > redInZone) this.state.teamScoreBlue = Math.min(this.state.teamScoreBlue + 1, 32767);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
_endTeamsRound() {
|
|
||||||
// Eliminate losing team
|
|
||||||
const redWins = this.state.teamScoreRed >= this.state.teamScoreBlue;
|
|
||||||
const losingTeam = redWins ? 2 : 1;
|
|
||||||
this.state.players.forEach((p, id) => {
|
|
||||||
if (p.team === losingTeam && !p.isEliminated) {
|
|
||||||
this._eliminatePlayer(id, "teams_lost");
|
|
||||||
} else if (!p.isEliminated) {
|
|
||||||
this._qualifyPlayer(id, "teams_won");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._endRound();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Elimination helpers ─────────────────────────────────────────────
|
// ─── Elimination helpers ─────────────────────────────────────────────
|
||||||
|
|
||||||
_eliminatePlayer(sessionId, reason) {
|
_eliminatePlayer(sessionId, reason) {
|
||||||
@@ -323,45 +224,23 @@ class ArenaRoom extends Room {
|
|||||||
this.broadcast("qualified", { sessionId, name: player.name });
|
this.broadcast("qualified", { sessionId, name: player.name });
|
||||||
console.log(`[ArenaRoom] ${player.name} (${sessionId}) qualified: ${reason}`);
|
console.log(`[ArenaRoom] ${player.name} (${sessionId}) qualified: ${reason}`);
|
||||||
|
|
||||||
if (this.state.gameMode === "race") {
|
const totalActive = this._getActiveCount();
|
||||||
const aliveCount = this._getAliveCount();
|
const qualifiedCount = this._getQualifiedCount();
|
||||||
const totalActive = this._getActiveCount();
|
const toQualify = Math.ceil(totalActive * QUALIFY_RATIO);
|
||||||
const qualifiedCount = this._getQualifiedCount();
|
if (qualifiedCount >= toQualify) {
|
||||||
// Eliminate once qualify_ratio reached
|
this.state.players.forEach((p, id) => {
|
||||||
const toQualify = Math.ceil(totalActive * QUALIFY_RATIO);
|
if (!p.isQualified && !p.isEliminated) {
|
||||||
if (qualifiedCount >= toQualify) {
|
this._eliminatePlayer(id, "too_slow");
|
||||||
this.state.players.forEach((p, id) => {
|
}
|
||||||
if (!p.isQualified && !p.isEliminated) {
|
});
|
||||||
this._eliminatePlayer(id, "too_slow");
|
this._endRound();
|
||||||
}
|
|
||||||
});
|
|
||||||
this._endRound();
|
|
||||||
}
|
|
||||||
} else if (this.state.gameMode === "survival") {
|
|
||||||
// In survival: only 1 qualifies (last one), rest get eliminated by zone
|
|
||||||
this._checkRoundEndCondition();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkRoundEndCondition() {
|
_checkRoundEndCondition() {
|
||||||
if (this.state.phase !== "playing") return;
|
if (this.state.phase !== "playing") return;
|
||||||
const alive = this._getAliveCount();
|
const alive = this._getAliveCount();
|
||||||
const qualified = this._getQualifiedCount();
|
if (alive === 0) this._endRound();
|
||||||
const total = this._getActiveCount();
|
|
||||||
|
|
||||||
if (this.state.gameMode === "survival") {
|
|
||||||
if (alive <= 1) {
|
|
||||||
// Qualify the last survivor
|
|
||||||
this.state.players.forEach((p, id) => {
|
|
||||||
if (!p.isEliminated && !p.isQualified) {
|
|
||||||
this._qualifyPlayer(id, "last_survivor");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._endRound();
|
|
||||||
}
|
|
||||||
} else if (alive === 0 || alive + qualified >= total) {
|
|
||||||
this._endRound();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_getAliveCount() {
|
_getAliveCount() {
|
||||||
@@ -377,7 +256,9 @@ class ArenaRoom extends Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getActiveCount() {
|
_getActiveCount() {
|
||||||
return this.state.players.size;
|
let n = 0;
|
||||||
|
this.state.players.forEach((p) => { if (!p.isEliminated) n++; });
|
||||||
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
_updatePlayersAlive() {
|
_updatePlayersAlive() {
|
||||||
@@ -387,8 +268,6 @@ class ArenaRoom extends Room {
|
|||||||
_clearAllTimers() {
|
_clearAllTimers() {
|
||||||
if (this._phaseTimer) { clearTimeout(this._phaseTimer); this._phaseTimer = null; }
|
if (this._phaseTimer) { clearTimeout(this._phaseTimer); this._phaseTimer = null; }
|
||||||
if (this._lobbyTimer) { clearTimeout(this._lobbyTimer); this._lobbyTimer = null; }
|
if (this._lobbyTimer) { clearTimeout(this._lobbyTimer); this._lobbyTimer = null; }
|
||||||
if (this._survivalInterval) { clearInterval(this._survivalInterval); this._survivalInterval = null; }
|
|
||||||
if (this._teamInterval) { clearInterval(this._teamInterval); this._teamInterval = null; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Spawn helper ────────────────────────────────────────────────────
|
// ─── Spawn helper ────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -3,88 +3,58 @@ const { Schema, MapSchema, defineTypes } = require("@colyseus/schema");
|
|||||||
class Player extends Schema {
|
class Player extends Schema {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.x = 0;
|
this.x = 0; this.y = 5; this.z = 0;
|
||||||
this.y = 5;
|
this.vx = 0; this.vy = 0; this.vz = 0;
|
||||||
this.z = 0;
|
this.rx = 0; this.ry = 0; this.rz = 0; this.rw = 1;
|
||||||
this.vx = 0;
|
|
||||||
this.vy = 0;
|
|
||||||
this.vz = 0;
|
|
||||||
this.rx = 0;
|
|
||||||
this.ry = 0;
|
|
||||||
this.rz = 0;
|
|
||||||
this.rw = 1;
|
|
||||||
this.t = 0;
|
this.t = 0;
|
||||||
this.name = "";
|
this.name = "";
|
||||||
this.colorR = 1;
|
this.colorR = 1; this.colorG = 1; this.colorB = 1;
|
||||||
this.colorG = 1;
|
this.avx = 0; this.avy = 0; this.avz = 0;
|
||||||
this.colorB = 1;
|
|
||||||
this.avx = 0;
|
|
||||||
this.avy = 0;
|
|
||||||
this.avz = 0;
|
|
||||||
// Game state
|
|
||||||
this.isEliminated = false;
|
this.isEliminated = false;
|
||||||
this.isQualified = false;
|
this.isQualified = false;
|
||||||
this.isReady = false;
|
this.isReady = false;
|
||||||
this.team = 0;
|
|
||||||
this.checkpointIndex = 0;
|
this.checkpointIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field order must match NetworkSchema.cs [Type(N)] indices exactly
|
||||||
defineTypes(Player, {
|
defineTypes(Player, {
|
||||||
x: "float32",
|
x: "float32", y: "float32", z: "float32", // 0-2
|
||||||
y: "float32",
|
vx: "float32", vy: "float32", vz: "float32", // 3-5
|
||||||
z: "float32",
|
rx: "float32", ry: "float32", rz: "float32", rw: "float32", // 6-9
|
||||||
vx: "float32",
|
t: "float64", // 10
|
||||||
vy: "float32",
|
name: "string", // 11
|
||||||
vz: "float32",
|
colorR: "float32", colorG: "float32", colorB: "float32", // 12-14
|
||||||
rx: "float32",
|
avx: "float32", avy: "float32", avz: "float32", // 15-17
|
||||||
ry: "float32",
|
isEliminated: "boolean", // 18
|
||||||
rz: "float32",
|
isQualified: "boolean", // 19
|
||||||
rw: "float32",
|
isReady: "boolean", // 20
|
||||||
t: "float64",
|
checkpointIndex: "int8", // 21
|
||||||
name: "string",
|
|
||||||
colorR: "float32",
|
|
||||||
colorG: "float32",
|
|
||||||
colorB: "float32",
|
|
||||||
avx: "float32",
|
|
||||||
avy: "float32",
|
|
||||||
avz: "float32",
|
|
||||||
isEliminated: "boolean",
|
|
||||||
isQualified: "boolean",
|
|
||||||
isReady: "boolean",
|
|
||||||
team: "int8",
|
|
||||||
checkpointIndex: "int8",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
class GameState extends Schema {
|
class GameState extends Schema {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.players = new MapSchema();
|
this.players = new MapSchema();
|
||||||
this.phase = "lobby";
|
this.phase = "lobby";
|
||||||
this.countdown = 0;
|
this.countdown = 0;
|
||||||
this.roundNumber = 1;
|
this.roundNumber = 1;
|
||||||
this.totalRounds = 3;
|
this.totalRounds = 3;
|
||||||
this.playersAlive = 0;
|
this.playersAlive = 0;
|
||||||
this.gameMode = "race";
|
this.gameMode = "race";
|
||||||
this.deathZoneY = -50;
|
this.winnerName = "";
|
||||||
this.teamScoreRed = 0;
|
|
||||||
this.teamScoreBlue = 0;
|
|
||||||
this.winnerName = "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineTypes(GameState, {
|
defineTypes(GameState, {
|
||||||
players: { map: Player },
|
players: { map: Player }, // 0
|
||||||
phase: "string",
|
phase: "string", // 1
|
||||||
countdown: "float32",
|
countdown: "float32", // 2
|
||||||
roundNumber: "int8",
|
roundNumber: "int8", // 3
|
||||||
totalRounds: "int8",
|
totalRounds: "int8", // 4
|
||||||
playersAlive: "int8",
|
playersAlive: "int8", // 5
|
||||||
gameMode: "string",
|
gameMode: "string", // 6
|
||||||
deathZoneY: "float32",
|
winnerName: "string", // 7
|
||||||
teamScoreRed: "int16",
|
|
||||||
teamScoreBlue: "int16",
|
|
||||||
winnerName: "string",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = { GameState, Player };
|
module.exports = { GameState, Player };
|
||||||
|
|||||||
101
rolld_backend/game/src/stats/StatsManager.js
Normal file
101
rolld_backend/game/src/stats/StatsManager.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const DATA_FILE = path.join(__dirname, "../../data/stats.json");
|
||||||
|
|
||||||
|
const VALID_KEYS = [
|
||||||
|
"totalDistance", "totalJumps", "maxSpeed", "bestRaceTime",
|
||||||
|
"racesPlayed", "qualifications", "eliminations",
|
||||||
|
"checkpointsTotal", "bumpsGiven", "totalPlaytime",
|
||||||
|
];
|
||||||
|
|
||||||
|
let _stats = {};
|
||||||
|
const _lastUpdate = new Map(); // name → timestamp, for rate-limiting
|
||||||
|
|
||||||
|
function _load() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(DATA_FILE)) {
|
||||||
|
_stats = JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[Stats] Failed to load stats.json, starting fresh:", e.message);
|
||||||
|
_stats = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _save() {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true });
|
||||||
|
fs.writeFileSync(DATA_FILE, JSON.stringify(_stats, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[Stats] Failed to save stats.json:", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _defaults() {
|
||||||
|
return {
|
||||||
|
totalDistance: 0,
|
||||||
|
totalJumps: 0,
|
||||||
|
maxSpeed: 0,
|
||||||
|
bestRaceTime: null,
|
||||||
|
racesPlayed: 0,
|
||||||
|
qualifications: 0,
|
||||||
|
eliminations: 0,
|
||||||
|
checkpointsTotal: 0,
|
||||||
|
bumpsGiven: 0,
|
||||||
|
totalPlaytime: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(name, delta) {
|
||||||
|
if (!name || typeof name !== "string" || name.length > 32) return false;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const last = _lastUpdate.get(name) || 0;
|
||||||
|
if (now - last < 5000) return false; // rate-limit: 1 update per 5s per player
|
||||||
|
_lastUpdate.set(name, now);
|
||||||
|
|
||||||
|
if (!_stats[name]) _stats[name] = _defaults();
|
||||||
|
const p = _stats[name];
|
||||||
|
|
||||||
|
for (const key of VALID_KEYS) {
|
||||||
|
if (delta[key] === undefined) continue;
|
||||||
|
const val = Number(delta[key]);
|
||||||
|
if (isNaN(val)) continue;
|
||||||
|
|
||||||
|
if (key === "maxSpeed") {
|
||||||
|
p.maxSpeed = Math.max(p.maxSpeed, val);
|
||||||
|
} else if (key === "bestRaceTime") {
|
||||||
|
if (val > 0 && (p.bestRaceTime === null || val < p.bestRaceTime)) {
|
||||||
|
p.bestRaceTime = val;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p[key] = (p[key] || 0) + val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAll() {
|
||||||
|
return Object.entries(_stats).map(([name, s]) => ({ name, ...s }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLeaderboard(key) {
|
||||||
|
if (!VALID_KEYS.includes(key)) return [];
|
||||||
|
return Object.entries(_stats)
|
||||||
|
.map(([name, s]) => ({ name, value: s[key] ?? 0 }))
|
||||||
|
.filter((e) => e.value !== null && e.value > 0)
|
||||||
|
.sort((a, b) => {
|
||||||
|
// bestRaceTime: lower is better
|
||||||
|
if (key === "bestRaceTime") return a.value - b.value;
|
||||||
|
return b.value - a.value;
|
||||||
|
})
|
||||||
|
.slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
_load();
|
||||||
|
console.log(`[Stats] Loaded ${Object.keys(_stats).length} player(s)`);
|
||||||
|
|
||||||
|
module.exports = { update, getAll, getLeaderboard, VALID_KEYS };
|
||||||
Reference in New Issue
Block a user