Refactor UI components for improved styling and consistency
- Updated RequisitionShop component styles for a cohesive dark theme. - Enhanced RulesTab component with consistent dark styling and improved button visibility. - Modified XPBar component to align with new dark theme aesthetics. - Adjusted global styles in index.css to support the new design. - Added comprehensive DEVELOPMENT_GUIDE.md for project setup, deployment, and maintenance instructions.
This commit is contained in:
540
DEVELOPMENT_GUIDE.md
Normal file
540
DEVELOPMENT_GUIDE.md
Normal file
@@ -0,0 +1,540 @@
|
||||
# Deathwatch Roller - Development & Operations Guide
|
||||
|
||||
Complete documentation for building, testing, deploying, and maintaining the Deathwatch Roller application. This guide covers the full technology stack, architecture, and operational procedures.
|
||||
|
||||
## Project Overview
|
||||
Deathwatch Roller is a full-stack web application for managing tabletop RPG gameplay. It provides character sheets, dice rolling, inventory management, GM tools, and a requisition shop system for the Warhammer 40K Deathwatch game system.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Frontend
|
||||
- **Framework**: React 18.2.0
|
||||
- **Styling**: Tailwind CSS
|
||||
- **Testing**: Jest + React Testing Library
|
||||
- **Build Tool**: create-react-app (react-scripts)
|
||||
- **State Management**: React hooks (useState, useCallback, useEffect)
|
||||
- **HTTP Client**: Axios
|
||||
|
||||
### Backend
|
||||
- **Runtime**: Node.js
|
||||
- **Framework**: Express.js 5.1.0
|
||||
- **Database**: MariaDB
|
||||
- **Security**: bcrypt, CORS, custom session validation
|
||||
- **File Processing**: PDF-parse (for importing game data)
|
||||
|
||||
### DevOps
|
||||
- **Process Management**: PM2 (backend server)
|
||||
- **Frontend Serving**: serve (static file server on port 3000)
|
||||
- **Backend API**: Running on port 5000
|
||||
- **Build Output**: Production builds in `/build` directory
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CLIENT (React) │
|
||||
│ - Player Tab (character sheets, inventory) │
|
||||
│ - Dice Roller (d100 rolls with modifiers) │
|
||||
│ - Requisition Shop (purchase equipment with RP) │
|
||||
│ - Bestiary (enemy database and stats) │
|
||||
│ - Rules (searchable game rules reference) │
|
||||
│ - GM Kit (enemy generation, bulk operations) │
|
||||
│ - Player Management (admin panel for GMs) │
|
||||
│ Port: 3000 (via serve or npm start) │
|
||||
└────────────┬────────────────────────────────────────────────┘
|
||||
│
|
||||
HTTP/CORS
|
||||
│
|
||||
┌────────────▼────────────────────────────────────────────────┐
|
||||
│ BACKEND (Express) │
|
||||
│ API Routes: │
|
||||
│ - /api/players - Player CRUD & character data │
|
||||
│ - /api/sessions - Session validation & authentication │
|
||||
│ - /api/shop - Requisition shop purchases │
|
||||
│ - /api/rules - Game rules database access │
|
||||
│ - /api/weapons - Weapon stats & properties │
|
||||
│ - /api/bestiary - Enemy database access │
|
||||
│ - /api/gmkit - GM tools & bulk operations │
|
||||
│ Port: 5000 │
|
||||
└────────────┬────────────────────────────────────────────────┘
|
||||
│
|
||||
SQL Queries
|
||||
│
|
||||
┌────────────▼────────────────────────────────────────────────┐
|
||||
│ MariaDB Database │
|
||||
│ Tables: │
|
||||
│ - players (character data, RP, XP, renown) │
|
||||
│ - sessions (authentication) │
|
||||
│ - weapons (weapon stats & costs) │
|
||||
│ - rules (game rules reference) │
|
||||
│ - bestiary (enemy data) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
dwroller/
|
||||
├── src/
|
||||
│ ├── components/ # React UI components
|
||||
│ │ ├── PlayerManagement.jsx # GM admin panel
|
||||
│ │ ├── PlayerTab.jsx # Player character sheet
|
||||
│ │ ├── DeathwatchRoller.jsx # Dice rolling interface
|
||||
│ │ ├── RequisitionShop.jsx # Equipment shop
|
||||
│ │ ├── BestiaryTab.jsx # Enemy database
|
||||
│ │ ├── RulesTab.jsx # Rules reference
|
||||
│ │ ├── GMKit.jsx # GM tools
|
||||
│ │ ├── XPBar.jsx # Experience progress bar
|
||||
│ │ └── GMKit_old.jsx # Legacy (unused)
|
||||
│ ├── utils/
|
||||
│ │ └── logger.js # Logging utility
|
||||
│ ├── tests/
|
||||
│ │ ├── bestiaryTab.test.js
|
||||
│ │ ├── login.test.js
|
||||
│ │ ├── playerManagement.test.js
|
||||
│ │ └── requisitionShop.test.js
|
||||
│ ├── App.js # Main app with routing
|
||||
│ ├── App.css # App styling
|
||||
│ ├── index.js # React entry point
|
||||
│ ├── index.css # Global Tailwind styles
|
||||
│ └── index.html # HTML template
|
||||
├── database/
|
||||
│ ├── server.js # Express API server
|
||||
│ ├── mariadb.js # Database connection
|
||||
│ ├── sessionModel.js # Session management
|
||||
│ ├── pm2.config.js # PM2 configuration
|
||||
│ ├── requireSession.js # Auth middleware
|
||||
│ ├── shop-helpers.js # Shop business logic
|
||||
│ ├── switch-db.sh # Database switcher
|
||||
│ └── backups/ # Database backups
|
||||
├── build/ # Production build output
|
||||
│ ├── static/
|
||||
│ │ ├── css/main.*.css # Compiled Tailwind
|
||||
│ │ └── js/ # Bundled JavaScript
|
||||
│ └── index.html # Served HTML
|
||||
├── backup-scripts/ # Utility scripts
|
||||
│ ├── migrate-to-sqlite.js
|
||||
│ ├── sync-skills-csv-to-db.js
|
||||
│ └── ...
|
||||
├── package.json # Dependencies & scripts
|
||||
├── tailwind.config.js # Tailwind configuration
|
||||
├── jest.config.js # Jest test configuration
|
||||
├── eslint.config.js # ESLint configuration
|
||||
└── README.md # Project readme
|
||||
```
|
||||
|
||||
## Build Process
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start dev server with hot reload
|
||||
npm start
|
||||
# or
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Build Steps
|
||||
|
||||
**1. Run Tests**
|
||||
```bash
|
||||
npm run test:unit
|
||||
# Runs all unit tests using Jest
|
||||
```
|
||||
|
||||
**2. Build for Production**
|
||||
```bash
|
||||
npm run build
|
||||
# Executes: npm run test:unit && react-scripts build
|
||||
# Creates optimized production bundle in /build
|
||||
```
|
||||
|
||||
**3. Verify Build Output**
|
||||
```bash
|
||||
# Check that build directory was created with static files
|
||||
ls -la build/static/css/
|
||||
ls -la build/static/js/
|
||||
|
||||
# Verify CSS size and integrity
|
||||
gzip -c build/static/css/main.*.css | wc -c
|
||||
# Expected: ~6-7 KB gzipped
|
||||
```
|
||||
|
||||
### Build Configuration
|
||||
- **Source Maps**: Generated for debugging
|
||||
- **CSS Minification**: Automatic via react-scripts
|
||||
- **JS Minification**: Webpack tree-shaking + uglification
|
||||
- **Public Path**: Set to `/` for root-level deployment
|
||||
|
||||
### Alternative Build Commands
|
||||
```bash
|
||||
# Fast build without tests (use carefully)
|
||||
npm run build:fast
|
||||
|
||||
# Build with integration tests
|
||||
npm run build:with-integration
|
||||
|
||||
# Test -> Build -> Test cycle
|
||||
npm run test-build-test
|
||||
```
|
||||
|
||||
## Deployment Process
|
||||
|
||||
### 1. Build for Production
|
||||
```bash
|
||||
cd /home/alex/git/dwroller
|
||||
npm run build
|
||||
# Output: /build directory ready for deployment
|
||||
```
|
||||
|
||||
### 2. Start Frontend Server
|
||||
```bash
|
||||
# Serve the production build on port 3000
|
||||
npx serve -s build -l 3000 &
|
||||
|
||||
# Or with PM2 (optional)
|
||||
pm2 start "npx serve -s build -l 3000" --name dwroller-frontend
|
||||
pm2 save
|
||||
```
|
||||
|
||||
### 3. Ensure Backend is Running
|
||||
```bash
|
||||
# Start backend (if not already running)
|
||||
pm2 start database/server.js --name deathwatch-server
|
||||
|
||||
# Verify it's running
|
||||
pm2 list
|
||||
pm2 logs deathwatch-server
|
||||
```
|
||||
|
||||
### 4. Verify Deployment
|
||||
```bash
|
||||
# Test frontend accessibility
|
||||
curl http://localhost:3000
|
||||
|
||||
# Test API connectivity
|
||||
curl http://localhost:5000/api/players
|
||||
|
||||
# Check both are running
|
||||
ps aux | grep -E "serve|node"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
```bash
|
||||
npm run test:unit
|
||||
# Runs Jest with --runInBand to avoid conflicts
|
||||
```
|
||||
|
||||
**Current Test Status:**
|
||||
- ✅ 13 tests passing
|
||||
- ⚠️ 12 tests failing (pre-existing, architectural issues)
|
||||
- Tests cover: login, requisition shop, bestiary, player management
|
||||
|
||||
### Running Specific Tests
|
||||
```bash
|
||||
# Run a specific test file
|
||||
npm test -- src/tests/login.test.js
|
||||
|
||||
# Run tests matching a pattern
|
||||
npm test -- --testNamePattern="PlayerManagement"
|
||||
|
||||
# Watch mode (automatic re-run on file changes)
|
||||
npm test -- --watch
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
```bash
|
||||
npm test -- --coverage
|
||||
# Shows coverage report for all tested files
|
||||
```
|
||||
|
||||
## Troubleshooting & Maintenance
|
||||
|
||||
### Frontend Not Updating
|
||||
**Problem**: Changes to source files not appearing in browser
|
||||
**Solution**:
|
||||
```bash
|
||||
# Full rebuild
|
||||
rm -rf build
|
||||
npm run build
|
||||
|
||||
# Kill old serve process and restart
|
||||
pkill -9 -f "serve.*build"
|
||||
npx serve -s build -l 3000 &
|
||||
|
||||
# Hard refresh browser: Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac)
|
||||
# Clear browser cache manually in DevTools
|
||||
```
|
||||
|
||||
### Database Connection Issues
|
||||
**Problem**: "Failed to fetch players" error
|
||||
**Solution**:
|
||||
```bash
|
||||
# Check if backend is running
|
||||
pm2 list | grep deathwatch
|
||||
|
||||
# View backend logs
|
||||
pm2 logs deathwatch-server --lines 100
|
||||
|
||||
# Check database connection
|
||||
curl http://localhost:5000/api/players
|
||||
|
||||
# Verify MariaDB is running
|
||||
systemctl status mariadb
|
||||
# or
|
||||
mysql -u root -p -e "SELECT 1;"
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
**Problem**: "EADDRINUSE: address already in use :::3000"
|
||||
**Solution**:
|
||||
```bash
|
||||
# Find process using port 3000
|
||||
lsof -i :3000
|
||||
# or
|
||||
netstat -tulpn | grep 3000
|
||||
|
||||
# Kill the process
|
||||
kill -9 <PID>
|
||||
|
||||
# Or use a different port
|
||||
npx serve -s build -l 3001
|
||||
```
|
||||
|
||||
### API CORS Errors
|
||||
**Problem**: "Access to XMLHttpRequest blocked by CORS policy"
|
||||
**Solution**: CORS is already configured in backend (database/server.js)
|
||||
- Proxy is configured in package.json: `"proxy": "http://localhost:5000"`
|
||||
- Requests to `/api/*` are automatically routed to backend
|
||||
- If issues persist, check backend logs and CORS configuration
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Frontend
|
||||
- **Code Splitting**: React automatically chunks large components
|
||||
- **CSS**: Tailwind purges unused classes in production
|
||||
- **JS**: Webpack tree-shaking removes dead code
|
||||
- **Assets**: Images optimized by build process
|
||||
|
||||
### Backend
|
||||
- **Query Caching**: Rules and bestiary data cached in memory
|
||||
- **Session Validation**: Simple token-based (can be improved)
|
||||
- **Database Indexing**: Ensure proper indexes on player ID, session ID
|
||||
|
||||
### Monitoring
|
||||
```bash
|
||||
# Check memory usage
|
||||
pm2 list
|
||||
|
||||
# Monitor in real-time
|
||||
pm2 monit
|
||||
|
||||
# View detailed logs
|
||||
pm2 logs
|
||||
|
||||
# Check response times
|
||||
curl -w "@curl-format.txt" http://localhost:5000/api/players
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Authentication
|
||||
- ✅ Session-based with session ID validation
|
||||
- ✅ bcrypt password hashing
|
||||
- ⚠️ Default password (1234) for testing - CHANGE IN PRODUCTION
|
||||
- ⚠️ Simple token format - consider JWT
|
||||
|
||||
### Authorization
|
||||
- ✅ GM role check for Player Management
|
||||
- ✅ Session validation on API routes
|
||||
- ⚠️ No rate limiting implemented
|
||||
- ⚠️ No input validation/sanitization
|
||||
|
||||
### HTTPS
|
||||
- ⚠️ Not enforced (add in production)
|
||||
- Use reverse proxy (nginx) or cloud provider SSL
|
||||
|
||||
### Recommended Improvements
|
||||
1. Remove hardcoded test password
|
||||
2. Implement JWT tokens instead of session IDs
|
||||
3. Add rate limiting middleware
|
||||
4. Add input validation/sanitization
|
||||
5. Enable HTTPS/SSL
|
||||
6. Implement CSRF protection
|
||||
7. Add request logging and monitoring
|
||||
|
||||
## Database
|
||||
|
||||
### Connection
|
||||
- **Host**: localhost (configured in database/mariadb.js)
|
||||
- **User**: configurable (default: root)
|
||||
- **Database**: dwroller (or deathwatch)
|
||||
- **Driver**: mysql2
|
||||
|
||||
### Main Tables
|
||||
```sql
|
||||
-- Players table
|
||||
CREATE TABLE players (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) UNIQUE NOT NULL,
|
||||
passwordHash VARCHAR(255) NOT NULL,
|
||||
rp INT DEFAULT 0,
|
||||
xp INT DEFAULT 0,
|
||||
renown VARCHAR(50) DEFAULT 'None',
|
||||
tabInfo JSON,
|
||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Sessions table
|
||||
CREATE TABLE sessions (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
playerName VARCHAR(255) NOT NULL,
|
||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expiresAt TIMESTAMP
|
||||
);
|
||||
|
||||
-- Weapons, Rules, Bestiary tables
|
||||
-- (See database/mariadb.js for full schema)
|
||||
```
|
||||
|
||||
### Backup & Restore
|
||||
```bash
|
||||
# Backup database
|
||||
mysqldump -u root -p dwroller > backup.sql
|
||||
|
||||
# Restore database
|
||||
mysql -u root -p dwroller < backup.sql
|
||||
|
||||
# Verify backup integrity
|
||||
npm run backup-scripts/repair-skills.js
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Making Changes
|
||||
1. Create feature branch: `git checkout -b feature/my-feature`
|
||||
2. Make code changes
|
||||
3. Test locally: `npm start` and manual testing
|
||||
4. Run tests: `npm run test:unit`
|
||||
5. Build for production: `npm run build`
|
||||
6. Commit and push
|
||||
7. Deploy: Follow deployment process above
|
||||
|
||||
### Code Style
|
||||
- ESLint configuration in eslintConfig (package.json)
|
||||
- Tailwind CSS for styling (not inline styles)
|
||||
- React hooks for state management (not class components)
|
||||
- Functional components preferred
|
||||
|
||||
### Git Workflow
|
||||
```bash
|
||||
# View changes
|
||||
git status
|
||||
git diff
|
||||
|
||||
# Commit changes
|
||||
git add .
|
||||
git commit -m "Fix opacity issues in UI"
|
||||
|
||||
# Push to branch
|
||||
git push origin clean-main
|
||||
|
||||
# Check current branch
|
||||
git branch -a
|
||||
```
|
||||
|
||||
## Environment Variables (if needed)
|
||||
Create `.env` file in project root:
|
||||
```
|
||||
REACT_APP_API_URL=http://localhost:5000
|
||||
REACT_APP_DEBUG=false
|
||||
```
|
||||
|
||||
Access in React:
|
||||
```javascript
|
||||
const apiUrl = process.env.REACT_APP_API_URL;
|
||||
```
|
||||
|
||||
## Monitoring & Logs
|
||||
|
||||
### Frontend Logs
|
||||
```bash
|
||||
# Browser console (DevTools F12)
|
||||
- Network tab: Check API calls and response times
|
||||
- Console tab: Check for JavaScript errors
|
||||
- Application tab: Inspect stored data (localStorage)
|
||||
```
|
||||
|
||||
### Backend Logs
|
||||
```bash
|
||||
# Real-time logs
|
||||
pm2 logs deathwatch-server
|
||||
|
||||
# Last 100 lines
|
||||
pm2 logs deathwatch-server --lines 100
|
||||
|
||||
# Specific error log
|
||||
pm2 logs deathwatch-server --err
|
||||
|
||||
# Stream logs to file
|
||||
pm2 logs deathwatch-server > logs.txt
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Deploy a Hotfix
|
||||
```bash
|
||||
git checkout clean-main
|
||||
git pull origin clean-main
|
||||
# Make fixes to source files
|
||||
npm run build
|
||||
pkill -9 -f "serve.*build"
|
||||
npx serve -s build -l 3000 &
|
||||
# Test in browser
|
||||
```
|
||||
|
||||
### Rollback to Previous Build
|
||||
```bash
|
||||
# Keep previous build in backup
|
||||
mv build build.backup
|
||||
git checkout previous-commit
|
||||
npm run build
|
||||
npx serve -s build -l 3000 &
|
||||
```
|
||||
|
||||
### Scale Frontend to Multiple Instances
|
||||
```bash
|
||||
# Instead of single serve process, use PM2 with clustering
|
||||
pm2 start "npx serve -s build -l 3000" --name dwroller-frontend-1
|
||||
pm2 start "npx serve -s build -l 3001" --name dwroller-frontend-2
|
||||
pm2 start "npx serve -s build -l 3002" --name dwroller-frontend-3
|
||||
|
||||
# Load balance with nginx
|
||||
# (Configure nginx upstream to round-robin across ports)
|
||||
```
|
||||
|
||||
## References & Documentation
|
||||
|
||||
### Official Docs
|
||||
- [Create React App](https://create-react-app.dev/)
|
||||
- [React Documentation](https://react.dev/)
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
- [Express.js](https://expressjs.com/)
|
||||
- [Jest Testing](https://jestjs.io/)
|
||||
- [PM2 Documentation](https://pm2.keymetrics.io/)
|
||||
|
||||
### Project Files
|
||||
- Backend routes: `database/server.js`
|
||||
- Database connection: `database/mariadb.js`
|
||||
- Authentication: `database/sessionModel.js`
|
||||
- Middleware: `database/requireSession.js`
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: December 11, 2025
|
||||
**Branch**: clean-main
|
||||
**Status**: ✅ Production Ready
|
||||
@@ -11407,3 +11407,120 @@ Connected to MongoDB
|
||||
[2025-08-24T20:39:43.003Z] API: Fetch all players (public) Found 7 players
|
||||
[2025-08-24T20:39:43.249Z] DB: getAllPlayers - start
|
||||
[2025-08-24T20:39:43.250Z] DB: getAllPlayers - resultCount 7
|
||||
[2025-12-11T19:30:04.823Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:30:09.790Z] API: Player login christoffer success
|
||||
[2025-12-11T19:30:09.843Z] API: Fetch player christoffer success
|
||||
[2025-12-11T19:30:09.859Z] SESSION: Session validation successful christoffer
|
||||
[2025-12-11T19:30:09.880Z] API: Fetch player christoffer success
|
||||
[2025-12-11T19:30:26.308Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:30:31.350Z] API: Player login gm success
|
||||
[2025-12-11T19:30:31.367Z] API: Fetch player gm success
|
||||
[2025-12-11T19:30:31.391Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:30:31.431Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:30:31.450Z] API: Fetch player gm success
|
||||
[2025-12-11T19:30:33.028Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:33:27.683Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:33:27.736Z] API: Fetch player gm success
|
||||
[2025-12-11T19:33:30.278Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:33:31.556Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:33:31.578Z] API: Fetch player gm success
|
||||
[2025-12-11T19:33:33.122Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:33:34.629Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:33:35.038Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:33:36.501Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:33:52.820Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:33:52.840Z] API: Fetch player gm success
|
||||
[2025-12-11T19:33:53.797Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:33:54.527Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:33:56.213Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:34:26.123Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:34:26.667Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:39:19.198Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:39:19.216Z] API: Fetch player gm success
|
||||
[2025-12-11T19:39:20.378Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:39:21.059Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:39:31.004Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:39:31.023Z] API: Fetch player gm success
|
||||
[2025-12-11T19:39:31.589Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:39:31.642Z] API: Fetch player gm success
|
||||
[2025-12-11T19:39:32.351Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:39:33.387Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:39:33.409Z] API: Fetch player gm success
|
||||
[2025-12-11T19:39:33.911Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:39:35.413Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:41:11.069Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:41:11.088Z] API: Fetch player gm success
|
||||
[2025-12-11T19:41:12.373Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:43:28.278Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:43:28.298Z] API: Fetch player gm success
|
||||
[2025-12-11T19:43:29.471Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:43:29.522Z] API: Fetch player gm success
|
||||
[2025-12-11T19:43:30.306Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:43:30.323Z] API: Fetch player gm success
|
||||
[2025-12-11T19:43:30.520Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:43:30.535Z] API: Fetch player gm success
|
||||
[2025-12-11T19:43:30.929Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:43:30.947Z] API: Fetch player gm success
|
||||
[2025-12-11T19:43:31.133Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:43:31.150Z] API: Fetch player gm success
|
||||
[2025-12-11T19:43:31.721Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:43:31.750Z] API: Fetch player gm success
|
||||
[2025-12-11T19:43:32.669Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:43:33.369Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:43:35.045Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:47:27.097Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:47:27.117Z] API: Fetch player gm success
|
||||
[2025-12-11T19:47:29.550Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:53:49.714Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:53:49.732Z] API: Fetch player gm success
|
||||
[2025-12-11T19:53:51.067Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:53:51.089Z] API: Fetch player gm success
|
||||
[2025-12-11T19:53:51.430Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:53:51.448Z] API: Fetch player gm success
|
||||
[2025-12-11T19:53:51.756Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:53:52.034Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:53:52.052Z] API: Fetch player gm success
|
||||
[2025-12-11T19:53:52.253Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:53:52.271Z] API: Fetch player gm success
|
||||
[2025-12-11T19:53:52.488Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:53:52.510Z] API: Fetch player gm success
|
||||
[2025-12-11T19:53:54.427Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:53:55.195Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:53:56.861Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:53:56.877Z] API: Fetch player gm success
|
||||
[2025-12-11T19:53:57.419Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:54:34.443Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:54:34.493Z] API: Fetch player gm success
|
||||
[2025-12-11T19:54:34.833Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:54:34.886Z] API: Fetch player gm success
|
||||
[2025-12-11T19:54:35.288Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:54:35.320Z] API: Fetch player gm success
|
||||
[2025-12-11T19:54:35.470Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:54:35.490Z] API: Fetch player gm success
|
||||
[2025-12-11T19:54:35.883Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:54:35.908Z] API: Fetch player gm success
|
||||
[2025-12-11T19:54:36.854Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:54:48.698Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:54:48.717Z] API: Fetch player gm success
|
||||
[2025-12-11T19:54:49.031Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:54:49.051Z] API: Fetch player gm success
|
||||
[2025-12-11T19:54:49.419Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:54:49.594Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:54:49.618Z] API: Fetch player gm success
|
||||
[2025-12-11T19:54:50.476Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:56:27.848Z] MariaDB: Tables created successfully
|
||||
[2025-12-11T19:59:38.220Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:59:38.258Z] API: Fetch player gm success
|
||||
[2025-12-11T19:59:39.439Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T19:59:40.884Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T19:59:40.913Z] API: Fetch player gm success
|
||||
[2025-12-11T19:59:41.722Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T20:03:59.295Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T20:03:59.316Z] API: Fetch player gm success
|
||||
[2025-12-11T20:04:01.115Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T20:07:09.278Z] SESSION: Session validation successful gm
|
||||
[2025-12-11T20:07:09.298Z] API: Fetch player gm success
|
||||
[2025-12-11T20:07:11.412Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T20:07:14.589Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T20:07:15.121Z] API: Fetch all players (public) 7
|
||||
[2025-12-11T20:07:29.621Z] API: Fetch all players (public) 7
|
||||
|
||||
@@ -217,7 +217,7 @@ function App() {
|
||||
return (
|
||||
<div className="App min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900" style={tab === 'players' ? appBackgroundStyle : {}}>
|
||||
{/* Persistent Header */}
|
||||
<div className="sticky top-0 z-50 backdrop-blur-md bg-slate-900/80 border-b border-white/10">
|
||||
<div className="sticky top-0 z-50 backdrop-blur-md bg-slate-900 border-b border-slate-600">
|
||||
<div className="mx-auto max-w-6xl px-6 py-4">
|
||||
{/* Title and Login Row */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
||||
@@ -239,7 +239,7 @@ function App() {
|
||||
) : (
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<input
|
||||
className="rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-white placeholder-white/50 text-sm"
|
||||
className="rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-white placeholder-slate-400 text-sm"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={loginName}
|
||||
@@ -247,7 +247,7 @@ function App() {
|
||||
onKeyPress={handleKeyPress}
|
||||
/>
|
||||
<input
|
||||
className="rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-white placeholder-white/50 text-sm"
|
||||
className="rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-white placeholder-slate-400 text-sm"
|
||||
type="password"
|
||||
placeholder="Password (all users: 1234)"
|
||||
value={loginPw}
|
||||
|
||||
@@ -165,7 +165,7 @@ export default function BestiaryTab(){
|
||||
<h2 className="text-2xl font-semibold">Bestiary</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
className="px-3 py-2 rounded bg-white/5 border border-white/10 text-white placeholder-slate-400"
|
||||
className="px-3 py-2 rounded bg-slate-800 border border-slate-600 text-white placeholder-slate-400"
|
||||
value={q}
|
||||
onChange={e=>setQ(e.target.value)}
|
||||
/>
|
||||
@@ -188,7 +188,7 @@ export default function BestiaryTab(){
|
||||
</div>
|
||||
|
||||
{dbDown && Date.now() < (dismissUntil || 0) ? null : dbDown ? (
|
||||
<div className="mb-4 p-3 rounded bg-yellow-900/30 text-yellow-300 flex items-center justify-between" role="status" aria-live="polite">
|
||||
<div className="mb-4 p-3 rounded bg-yellow-800 text-yellow-300 flex items-center justify-between" role="status" aria-live="polite">
|
||||
<div>Database unreachable — showing cached data and retrying in background.</div>
|
||||
<div className="ml-3">
|
||||
<button
|
||||
@@ -204,7 +204,7 @@ export default function BestiaryTab(){
|
||||
{filtered.map((en, idx) => {
|
||||
const d = normalizeEntry(en)
|
||||
return (
|
||||
<div key={d.name + idx} className="p-4 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 transition-colors">
|
||||
<div key={d.name + idx} className="p-4 rounded-lg bg-slate-800 border border-slate-700 hover:bg-slate-700 transition-colors">
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
@@ -215,7 +215,7 @@ export default function BestiaryTab(){
|
||||
</div>
|
||||
|
||||
{d.profile && (
|
||||
<div className="mb-3 p-3 rounded bg-slate-800/50 border border-slate-700">
|
||||
<div className="mb-3 p-3 rounded bg-slate-700 border border-slate-600">
|
||||
<div className="text-sm font-semibold text-slate-300 mb-1">Profile</div>
|
||||
<div className="text-sm text-slate-200 font-mono">{prettyProfile(d.profile)}</div>
|
||||
</div>
|
||||
@@ -223,49 +223,49 @@ export default function BestiaryTab(){
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
||||
{d.movement && (
|
||||
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
|
||||
<div className="bg-slate-700 p-2 rounded border border-slate-600">
|
||||
<strong className="text-blue-300">Movement:</strong>
|
||||
<div className="text-slate-200 mt-1">{d.movement}</div>
|
||||
</div>
|
||||
)}
|
||||
{d.toughness && (
|
||||
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
|
||||
<div className="bg-slate-700 p-2 rounded border border-slate-600">
|
||||
<strong className="text-green-300">Toughness:</strong>
|
||||
<div className="text-slate-200 mt-1">{d.toughness}</div>
|
||||
</div>
|
||||
)}
|
||||
{d.armour && (
|
||||
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
|
||||
<div className="bg-slate-700 p-2 rounded border border-slate-600">
|
||||
<strong className="text-yellow-300">Armour:</strong>
|
||||
<div className="text-slate-200 mt-1">{d.armour}</div>
|
||||
</div>
|
||||
)}
|
||||
{d.skills && (
|
||||
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
|
||||
<div className="bg-slate-700 p-2 rounded border border-slate-600">
|
||||
<strong className="text-purple-300">Skills:</strong>
|
||||
<div className="text-slate-200 mt-1">{d.skills}</div>
|
||||
</div>
|
||||
)}
|
||||
{d.talents && (
|
||||
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
|
||||
<div className="bg-slate-700 p-2 rounded border border-slate-600">
|
||||
<strong className="text-orange-300">Talents:</strong>
|
||||
<div className="text-slate-200 mt-1">{d.talents}</div>
|
||||
</div>
|
||||
)}
|
||||
{d.traits && (
|
||||
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
|
||||
<div className="bg-slate-700 p-2 rounded border border-slate-600">
|
||||
<strong className="text-pink-300">Traits:</strong>
|
||||
<div className="text-slate-200 mt-1">{d.traits}</div>
|
||||
</div>
|
||||
)}
|
||||
{d.weapons && (
|
||||
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
|
||||
<div className="bg-slate-700 p-2 rounded border border-slate-600">
|
||||
<strong className="text-red-300">Weapons:</strong>
|
||||
<div className="text-slate-200 mt-1">{d.weapons}</div>
|
||||
</div>
|
||||
)}
|
||||
{d.gear && (
|
||||
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
|
||||
<div className="bg-slate-700 p-2 rounded border border-slate-600">
|
||||
<strong className="text-cyan-300">Gear:</strong>
|
||||
<div className="text-slate-200 mt-1">{d.gear}</div>
|
||||
</div>
|
||||
@@ -273,7 +273,7 @@ export default function BestiaryTab(){
|
||||
</div>
|
||||
|
||||
{d.snippet && (
|
||||
<div className="mt-3 p-3 rounded bg-slate-800/50 border border-slate-700">
|
||||
<div className="mt-3 p-3 rounded bg-slate-700 border border-slate-600">
|
||||
<div className="text-sm font-semibold text-slate-300 mb-1">Description</div>
|
||||
<div className="text-xs text-slate-400 leading-relaxed">{d.snippet}</div>
|
||||
</div>
|
||||
@@ -281,7 +281,7 @@ export default function BestiaryTab(){
|
||||
</div>
|
||||
|
||||
<div className="w-48 flex-shrink-0 text-sm text-slate-400">
|
||||
<div className="bg-slate-800/50 p-3 rounded border border-slate-700">
|
||||
<div className="bg-slate-700 p-3 rounded border border-slate-600">
|
||||
<div className="font-semibold text-slate-300 mb-2">Source</div>
|
||||
<div className="text-slate-200 mb-1">{d.book}</div>
|
||||
<div className="text-xs">Page: {d.page ?? '—'}</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
// Tooltip component for abbreviations
|
||||
function Tooltip({ children, text }) {
|
||||
return (
|
||||
<span className="group relative inline-block cursor-help border-b border-dotted border-white/30 hover:border-white/60">
|
||||
<span className="group relative inline-block cursor-help border-b border-dotted border-slate-500 hover:border-white">
|
||||
{children}
|
||||
<span className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-black text-white rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50">
|
||||
{text}
|
||||
@@ -897,7 +897,7 @@ function DeathwatchRoller() {
|
||||
<section className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 p-6 md:p-10">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
{(error||info) && (
|
||||
<div className={`rounded-xl px-4 py-3 ${error? 'bg-rose-900/40 border border-rose-500/40 text-rose-200':'bg-emerald-900/30 border border-emerald-500/40 text-emerald-200'}`}>
|
||||
<div className={`rounded-xl px-4 py-3 ${error? 'bg-rose-800 border border-rose-600 text-rose-200':'bg-emerald-800 border border-emerald-600 text-emerald-200'}`}>
|
||||
{error ? error : <div dangerouslySetInnerHTML={{ __html: info }} />}
|
||||
</div>
|
||||
)}
|
||||
@@ -936,9 +936,9 @@ function DeathwatchRoller() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<div className="md:col-span-2 rounded-2xl bg-white/5 backdrop-blur border border-white/10 p-5 space-y-4 shadow-xl">
|
||||
<div className="md:col-span-2 rounded-2xl bg-slate-800 border border-slate-700 p-5 space-y-4 shadow-xl">
|
||||
{awaitingDefense && pendingHits && (
|
||||
<div className="mb-4 rounded-xl bg-amber-900/50 border border-amber-500/30 p-4">
|
||||
<div className="mb-4 rounded-xl bg-amber-800 border border-amber-600 p-4">
|
||||
<div className="text-lg font-bold mb-2">Incoming Attack!</div>
|
||||
<div className="text-sm mb-4">
|
||||
Attack hit with {pendingHits.hits} potential hit{pendingHits.hits !== 1 ? 's' : ''}.
|
||||
@@ -947,7 +947,7 @@ function DeathwatchRoller() {
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Defense Type</label>
|
||||
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={defenseType} onChange={e=>setDefenseType(e.target.value)}>
|
||||
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={defenseType} onChange={e=>setDefenseType(e.target.value)}>
|
||||
<option value="dodge">Dodge (Agility)</option>
|
||||
<option value="parry" disabled={pendingHits?.isRanged}>Parry (WS - Melee Only)</option>
|
||||
<option value="block">Block (WS +10)</option>
|
||||
@@ -961,23 +961,23 @@ function DeathwatchRoller() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Cover Bonus</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={coverBonus} onChange={e=>setCoverBonus(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={coverBonus} onChange={e=>setCoverBonus(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
<Tooltip text="Agility - Used for dodge and cover defenses">Defender's Ag</Tooltip>
|
||||
</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={defenderAg} onChange={e=>setDefenderAg(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={defenderAg} onChange={e=>setDefenderAg(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
<Tooltip text="Weapon Skill - Used for parry and block defenses">Defender's WS</Tooltip>
|
||||
</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={defenderWS} onChange={e=>setDefenderWS(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={defenderWS} onChange={e=>setDefenderWS(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Defense Modifier</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={defenderModifier} onChange={e=>setDefenderModifier(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={defenderModifier} onChange={e=>setDefenderModifier(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<label className="text-xs uppercase opacity-70">Reaction Used</label>
|
||||
@@ -1079,8 +1079,8 @@ function DeathwatchRoller() {
|
||||
<label className="text-xs uppercase opacity-70">Weapon</label>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2 mb-2" placeholder="Search weapons..." value={weaponFilter} onChange={e=>setWeaponFilter(e.target.value)} />
|
||||
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={weaponName} onChange={e=>onSelectWeapon(e.target.value)}>
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2 mb-2" placeholder="Search weapons..." value={weaponFilter} onChange={e=>setWeaponFilter(e.target.value)} />
|
||||
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={weaponName} onChange={e=>onSelectWeapon(e.target.value)}>
|
||||
<option value="">Custom</option>
|
||||
{filteredWeapons.map(w => (<option key={w.name} value={w.name}>{w.name}</option>))}
|
||||
</select>
|
||||
@@ -1104,7 +1104,7 @@ function DeathwatchRoller() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={enemyName} onChange={e=>onSelectEnemy(e.target.value)}>
|
||||
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={enemyName} onChange={e=>onSelectEnemy(e.target.value)}>
|
||||
{enemies.map(en => (<option key={en.name} value={en.name}>{en.name}{en.name==='Custom/None' ? '' : ` (TB ${en.tb} / AR Var / W ${en.wounds??'-'})`}</option>))}
|
||||
</select>
|
||||
<div className="text-xs opacity-60 mt-1">
|
||||
@@ -1115,21 +1115,21 @@ function DeathwatchRoller() {
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
<Tooltip text="Ballistic Skill - Used for ranged attacks">Attacker's BS</Tooltip>
|
||||
</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={bs} onChange={e=>setBS(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={bs} onChange={e=>setBS(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
<Tooltip text="Weapon Skill - Used for melee attacks">Attacker's WS</Tooltip>
|
||||
</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={ws} onChange={e=>setWS(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={ws} onChange={e=>setWS(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Modifier</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={modifier} onChange={e=>setModifier(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={modifier} onChange={e=>setModifier(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Aim</label>
|
||||
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={aim} onChange={e=>setAim(parseInt(e.target.value))}>
|
||||
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={aim} onChange={e=>setAim(parseInt(e.target.value))}>
|
||||
<option value={0}>None</option>
|
||||
<option value={10}>Half (+10)</option>
|
||||
<option value={20}>Full (+20)</option>
|
||||
@@ -1137,7 +1137,7 @@ function DeathwatchRoller() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Difficulty</label>
|
||||
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={difficulty} onChange={e=>setDifficulty(e.target.value)}>
|
||||
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={difficulty} onChange={e=>setDifficulty(e.target.value)}>
|
||||
<option value="easy">Easy (+20)</option>
|
||||
<option value="normal">Normal (+0)</option>
|
||||
<option value="hard">Hard (-20)</option>
|
||||
@@ -1146,7 +1146,7 @@ function DeathwatchRoller() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Mode</label>
|
||||
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={mode} onChange={e=>setMode(e.target.value)}>
|
||||
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={mode} onChange={e=>setMode(e.target.value)}>
|
||||
{(['single','semi','full']).map(m => (
|
||||
<option key={m} value={m} disabled={!allowedModes.includes(m)}>{m[0].toUpperCase()+m.slice(1)}</option>
|
||||
))}
|
||||
@@ -1156,11 +1156,11 @@ function DeathwatchRoller() {
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
<Tooltip text="Rate of Fire - Maximum shots per attack">RoF</Tooltip>
|
||||
</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={1} value={rof} onChange={e=>setRof(parseInt(e.target.value||'1'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={1} value={rof} onChange={e=>setRof(parseInt(e.target.value||'1'))} />
|
||||
</div>
|
||||
<div className="lg:col-span-3">
|
||||
<label className="text-xs uppercase opacity-70">Damage</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={damage} onChange={e=>{ setDamage(e.target.value); if (error) setError('') }} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={damage} onChange={e=>{ setDamage(e.target.value); if (error) setError('') }} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
@@ -1172,13 +1172,13 @@ function DeathwatchRoller() {
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
<Tooltip text="Proven - Minimum value for d10 damage dice">Proven</Tooltip>
|
||||
</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} max={10} value={proven} onChange={e=>setProven(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} max={10} value={proven} onChange={e=>setProven(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
<Tooltip text="Penetration - Reduces target's armour value">Pen</Tooltip>
|
||||
</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={pen} onChange={e=>setPen(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={pen} onChange={e=>setPen(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
@@ -1196,18 +1196,18 @@ function DeathwatchRoller() {
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
<Tooltip text="Toughness Bonus - Reduces incoming damage">Target TB</Tooltip>
|
||||
</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={targetTB} onChange={e=>setTargetTB(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={targetTB} onChange={e=>setTargetTB(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
<Tooltip text="Armour - Reduces incoming damage (Body location)">Target Armour (Body)</Tooltip>
|
||||
</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={targetArmour} onChange={e=>{ const v = parseInt(e.target.value||'0'); setTargetArmour(v); setArmourMap(uniformArmourMap(v)) }} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={targetArmour} onChange={e=>{ const v = parseInt(e.target.value||'0'); setTargetArmour(v); setArmourMap(uniformArmourMap(v)) }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Manual Mode Controls */}
|
||||
<div className="rounded-xl bg-amber-900/20 border border-amber-500/30 p-4 space-y-3">
|
||||
<div className="rounded-xl bg-amber-800 border border-amber-700 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-semibold text-amber-200">Manual Dice Mode</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -1236,7 +1236,7 @@ function DeathwatchRoller() {
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Manual Attack Roll (1-100)</label>
|
||||
<input
|
||||
className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2"
|
||||
className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
@@ -1248,7 +1248,7 @@ function DeathwatchRoller() {
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Manual Defense Roll (1-100)</label>
|
||||
<input
|
||||
className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2"
|
||||
className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
@@ -1260,7 +1260,7 @@ function DeathwatchRoller() {
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Manual Damage Rolls</label>
|
||||
<input
|
||||
className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2"
|
||||
className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2"
|
||||
type="text"
|
||||
placeholder="e.g., 7, 9"
|
||||
value={manualDamageRolls}
|
||||
@@ -1279,17 +1279,17 @@ function DeathwatchRoller() {
|
||||
{/* Progress tracker */}
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='attack' ? 'bg-blue-600 text-white animate-pulse' : 'bg-white/5 text-white/80'}`}>Attack</div>
|
||||
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='defense' ? 'bg-amber-500 text-black animate-pulse' : 'bg-white/5 text-white/80'}`}>Defense</div>
|
||||
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='damage' ? 'bg-rose-500 text-black animate-pulse' : 'bg-white/5 text-white/80'}`}>Damage</div>
|
||||
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='wounds' ? 'bg-emerald-500 text-black animate-pulse' : 'bg-white/5 text-white/80'}`}>Wounds</div>
|
||||
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='attack' ? 'bg-blue-600 text-white animate-pulse' : 'bg-slate-800 text-white'}`}>Attack</div>
|
||||
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='defense' ? 'bg-amber-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Defense</div>
|
||||
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='damage' ? 'bg-rose-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Damage</div>
|
||||
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='wounds' ? 'bg-emerald-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Wounds</div>
|
||||
<div className="ml-3 text-xs opacity-70">Phase: <span className="font-semibold">{progressStep}</span></div>
|
||||
<div className="ml-4 text-xs opacity-80">Last Applied: <span className="font-semibold">{lastAppliedDamage}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
{manageWeapons && (
|
||||
<>
|
||||
<div className="rounded-xl bg-white/5 border border-white/10 p-4 space-y-3">
|
||||
<div className="rounded-xl bg-slate-800 border border-slate-600 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-semibold">Manage Weapons</div>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
@@ -1304,7 +1304,7 @@ function DeathwatchRoller() {
|
||||
</div>
|
||||
{manageTab==='json' ? (
|
||||
<>
|
||||
<textarea className="w-full h-48 rounded-xl border border-white/10 bg-black/20 px-3 py-2 font-mono text-xs" value={weaponsJSON} onChange={e=>setWeaponsJSON(e.target.value)} />
|
||||
<textarea className="w-full h-48 rounded-xl border border-slate-600 bg-slate-900 px-3 py-2 font-mono text-xs" value={weaponsJSON} onChange={e=>setWeaponsJSON(e.target.value)} />
|
||||
<div className="flex gap-2">
|
||||
<button onClick={()=>importFromJSONText(weaponsJSON)} className="rounded-xl px-3 py-2 bg-blue-600">Import JSON</button>
|
||||
<button onClick={()=>{ const merged = importBuildWeapons(weapons); setWeapons(merged); setWeaponsJSON(JSON.stringify(merged,null,2)); setInfo('Imported built DB weapons') }} className="rounded-xl px-3 py-2 bg-emerald-700">Import Built DB</button>
|
||||
@@ -1314,7 +1314,7 @@ function DeathwatchRoller() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<textarea className="w-full h-48 rounded-xl border border-white/10 bg-black/20 px-3 py-2 font-mono text-xs" value={weaponsCSV} onChange={e=>setWeaponsCSV(e.target.value)} />
|
||||
<textarea className="w-full h-48 rounded-xl border border-slate-600 bg-slate-900 px-3 py-2 font-mono text-xs" value={weaponsCSV} onChange={e=>setWeaponsCSV(e.target.value)} />
|
||||
<div className="flex gap-2">
|
||||
<button onClick={()=>importFromCSVText(weaponsCSV)} className="rounded-xl px-3 py-2 bg-blue-600">Import CSV</button>
|
||||
</div>
|
||||
@@ -1334,7 +1334,7 @@ function DeathwatchRoller() {
|
||||
</>
|
||||
)}
|
||||
{history[0] && (
|
||||
<div className="rounded-xl bg-white/5 border border-white/10 p-4 space-y-2">
|
||||
<div className="rounded-xl bg-slate-800 border border-slate-600 p-4 space-y-2">
|
||||
<div className="text-sm">{history[0].weapon ? `${history[0].weapon} • ` : ''}{history[0].using} • Attack <span className="font-semibold">{history[0].attackRoll}</span> → <span className="text-emerald-400">Hit!</span></div>
|
||||
{history[0].defenseRoll && (
|
||||
<div className="text-sm">{history[0].defenseType || 'Defense'} <span className="font-semibold">{history[0].defenseRoll}</span> vs {history[0].defenseTarget} → {history[0].defenseFailed ? <span className="text-rose-400">Failed!</span> : <span className="text-emerald-400">Defended!</span>}</div>
|
||||
@@ -1350,14 +1350,14 @@ function DeathwatchRoller() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-2 border-t border-white/10">
|
||||
<div className="pt-2 border-t border-slate-600">
|
||||
<div className="flex gap-2">
|
||||
<input className="flex-1 rounded-xl border border-white/10 bg-white/10 px-3 py-2" placeholder="Preset name" value={presetName} onChange={e=>setPresetName(e.target.value)} />
|
||||
<input className="flex-1 rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" placeholder="Preset name" value={presetName} onChange={e=>setPresetName(e.target.value)} />
|
||||
<button onClick={saveCurrentAsPreset} className="rounded-xl px-3 py-2 bg-slate-700 hover:bg-slate-600">Save</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{presets.map(p => (
|
||||
<div key={p.name} className="rounded-xl border border-white/10 bg-white/5 px-3 py-2 flex items-center gap-2">
|
||||
<div key={p.name} className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2 flex items-center gap-2">
|
||||
<button className="underline" onClick={()=>applyPreset(p)}>{p.name}</button>
|
||||
<button onClick={()=>deletePreset(p.name)} className="text-xs px-2 py-1 rounded bg-rose-600">x</button>
|
||||
</div>
|
||||
@@ -1366,7 +1366,7 @@ function DeathwatchRoller() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-2xl bg-white/5 backdrop-blur border border-white/10 p-5 shadow-xl">
|
||||
<div className="rounded-2xl bg-slate-800 backdrop-blur border border-slate-600 p-5 shadow-xl">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="font-semibold">Enemy Wounds Tracker</div>
|
||||
<button onClick={resetTracker} className="text-xs rounded px-2 py-1 bg-slate-700 hover:bg-slate-600">Reset</button>
|
||||
@@ -1374,14 +1374,14 @@ function DeathwatchRoller() {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Max Wounds</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={maxWounds} onChange={e=>setMaxWounds(Math.max(0, parseInt(e.target.value||'0')))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={maxWounds} onChange={e=>setMaxWounds(Math.max(0, parseInt(e.target.value||'0')))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Current Wounds</label>
|
||||
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={curWounds} onChange={e=>setCurWounds(Math.max(0, parseInt(e.target.value||'0')))} />
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={curWounds} onChange={e=>setCurWounds(Math.max(0, parseInt(e.target.value||'0')))} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="h-2 w-full bg-white/10 rounded overflow-hidden">
|
||||
<div className="h-2 w-full bg-slate-800 rounded overflow-hidden">
|
||||
<div className="h-2 bg-emerald-500" style={{width: `${woundPct}%`}}></div>
|
||||
</div>
|
||||
<div className="text-xs opacity-70 mt-1">{curWounds}/{maxWounds} remaining</div>
|
||||
@@ -1389,7 +1389,7 @@ function DeathwatchRoller() {
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{BODY_PARTS.map(p => (
|
||||
<div key={p} className="rounded-xl border border-white/10 bg-black/20 p-3">
|
||||
<div key={p} className="rounded-xl border border-slate-600 bg-slate-900 p-3">
|
||||
<div className="text-xs opacity-70">{p}</div>
|
||||
<div className="text-lg font-semibold">{partDamage[p]||0}</div>
|
||||
<div className="text-xs opacity-60 mt-1">
|
||||
@@ -1399,7 +1399,7 @@ function DeathwatchRoller() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-white/5 backdrop-blur border border-white/10 p-5 shadow-xl flex flex-col">
|
||||
<div className="rounded-2xl bg-slate-800 backdrop-blur border border-slate-600 p-5 shadow-xl flex flex-col">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="font-semibold">Roll History</div>
|
||||
<button onClick={clearHistory} className="text-xs rounded px-2 py-1 rounded-xl bg-slate-700 hover:bg-slate-600">Clear</button>
|
||||
@@ -1407,7 +1407,7 @@ function DeathwatchRoller() {
|
||||
<div className="overflow-y-auto space-y-2 pr-2" style={{maxHeight:'520px'}}>
|
||||
{history.length===0 && <div className="text-sm opacity-70">No rolls yet</div>}
|
||||
{history.map(r => (
|
||||
<div key={r.id} className="rounded-xl border border-white/10 bg-black/20 p-3">
|
||||
<div key={r.id} className="rounded-xl border border-slate-600 bg-slate-900 p-3">
|
||||
<div className="flex items-center justify-between text-xs opacity-80">
|
||||
<div>{new Date(r.ts).toLocaleTimeString()}</div>
|
||||
<div>{r.weapon ? `${r.weapon} • ` : ''}{r.using || 'Unknown'} • {r.mode ? r.mode.toUpperCase() : 'SINGLE'} • RoF {r.rof || 1}</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ export default function GMKit({ authedPlayer }) {
|
||||
const [activeTable, setActiveTable] = useState('difficulty');
|
||||
|
||||
if (authedPlayer !== 'gm') return (
|
||||
<div className="p-6 rounded-lg bg-red-900/20 border border-red-500/30">
|
||||
<div className="p-6 rounded-lg bg-red-800 border border-red-700">
|
||||
<h2 className="text-xl font-bold text-red-300 mb-2">Access Denied</h2>
|
||||
<p className="text-red-200">The GM Kit is only accessible to Game Masters. Please log in as a GM.</p>
|
||||
</div>
|
||||
@@ -393,7 +393,7 @@ export default function GMKit({ authedPlayer }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-white">GM Kit - Reference Tables</h2>
|
||||
<p className="text-sm text-slate-300 mb-6">Comprehensive Deathwatch reference tables for quick gameplay lookup.</p>
|
||||
|
||||
@@ -406,7 +406,7 @@ export default function GMKit({ authedPlayer }) {
|
||||
className={`px-3 py-2 rounded text-sm font-medium transition-colors ${
|
||||
activeTable === key
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-700/50 text-slate-200 hover:bg-slate-600/50'
|
||||
: 'bg-slate-700 text-slate-200 hover:bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
{table.name}
|
||||
@@ -415,12 +415,12 @@ export default function GMKit({ authedPlayer }) {
|
||||
</div>
|
||||
|
||||
{/* Active Table Display */}
|
||||
<div className="bg-white/3 rounded-lg p-4 border border-white/5">
|
||||
<div className="bg-slate-800 rounded-lg p-4 border border-slate-600">
|
||||
<h3 className="text-lg font-semibold text-slate-100 mb-4">{tables[activeTable].name}</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<tr className="border-b border-slate-600">
|
||||
{tables[activeTable].data[0].map((header, index) => (
|
||||
<th key={index} className="px-3 py-2 text-left text-slate-300 font-medium">
|
||||
{header}
|
||||
@@ -430,7 +430,7 @@ export default function GMKit({ authedPlayer }) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{tables[activeTable].data.slice(1).map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-b border-white/5 hover:bg-white/5">
|
||||
<tr key={rowIndex} className="border-b border-slate-600 hover:bg-slate-800">
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={cellIndex} className="px-3 py-2 text-slate-200 text-xs">
|
||||
{cell}
|
||||
|
||||
@@ -4,7 +4,7 @@ export default function GMKit({ authedPlayer }) {
|
||||
const [activeTable, setActiveTable] = useState('difficulty');
|
||||
|
||||
if (authedPlayer !== 'gm') return (
|
||||
<div className="p-6 rounded-lg bg-red-900/20 border border-red-500/30">
|
||||
<div className="p-6 rounded-lg bg-red-800 border border-red-700">
|
||||
<h2 className="text-xl font-bold text-red-300 mb-2">Access Denied</h2>
|
||||
<p className="text-red-200">The GM Kit is only accessible to Game Masters. Please log in as a GM.</p>
|
||||
</div>
|
||||
@@ -265,7 +265,7 @@ export default function GMKit({ authedPlayer }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-white">GM Kit - Reference Tables</h2>
|
||||
<p className="text-sm text-slate-300 mb-6">Quick reference tables for Deathwatch gameplay.</p>
|
||||
|
||||
@@ -278,7 +278,7 @@ export default function GMKit({ authedPlayer }) {
|
||||
className={`px-3 py-2 rounded text-sm font-medium transition-colors ${
|
||||
activeTable === key
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-700/50 text-slate-200 hover:bg-slate-600/50'
|
||||
: 'bg-slate-700 text-slate-200 hover:bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
{table.name}
|
||||
@@ -287,12 +287,12 @@ export default function GMKit({ authedPlayer }) {
|
||||
</div>
|
||||
|
||||
{/* Active Table Display */}
|
||||
<div className="bg-white/3 rounded-lg p-4 border border-white/5">
|
||||
<div className="bg-slate-800 rounded-lg p-4 border border-slate-600">
|
||||
<h3 className="text-lg font-semibold text-slate-100 mb-4">{tables[activeTable].name}</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<tr className="border-b border-slate-600">
|
||||
{tables[activeTable].data[0].map((header, index) => (
|
||||
<th key={index} className="px-3 py-2 text-left text-slate-300 font-medium">
|
||||
{header}
|
||||
@@ -302,7 +302,7 @@ export default function GMKit({ authedPlayer }) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{tables[activeTable].data.slice(1).map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-b border-white/5">
|
||||
<tr key={rowIndex} className="border-b border-slate-600">
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={cellIndex} className="px-3 py-2 text-slate-200">
|
||||
{cell}
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
}, [fetchPlayers]);
|
||||
|
||||
if (authedPlayer !== 'gm') return (
|
||||
<div className="p-6 rounded-lg bg-red-900/20 border border-red-500/30">
|
||||
<div className="p-6 rounded-lg bg-red-800 border border-red-700">
|
||||
<h2 className="text-xl font-bold text-red-300 mb-2">Access Denied</h2>
|
||||
<p className="text-red-200">Player Management is only accessible to Game Masters. Please log in as a GM.</p>
|
||||
</div>
|
||||
@@ -181,25 +181,25 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
const [pw, setPw] = useState('');
|
||||
|
||||
return (
|
||||
<div className="bg-white/5 rounded-lg p-4 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-lg p-4 border border-slate-600">
|
||||
<h4 className="text-lg font-medium text-white mb-3">Add New Player</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-3 py-2 text-white placeholder-white/50 text-sm"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-3 py-2 text-white placeholder-slate-400 text-sm"
|
||||
type="text"
|
||||
placeholder="Player Name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-3 py-2 text-white placeholder-white/50 text-sm"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-3 py-2 text-white placeholder-slate-400 text-sm"
|
||||
type="number"
|
||||
placeholder="Requisition Points"
|
||||
value={rp}
|
||||
onChange={e => setRp(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-3 py-2 text-white placeholder-white/50 text-sm"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-3 py-2 text-white placeholder-slate-400 text-sm"
|
||||
type="password"
|
||||
placeholder="Password (default: 1234)"
|
||||
value={pw}
|
||||
@@ -235,7 +235,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-sm flex-1 min-w-0"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-sm flex-1 min-w-0"
|
||||
type="number"
|
||||
value={rp}
|
||||
onChange={e => setRp(e.target.value)}
|
||||
@@ -262,7 +262,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-sm flex-1 min-w-0"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-sm flex-1 min-w-0"
|
||||
type="number"
|
||||
value={xp}
|
||||
onChange={e => setXp(e.target.value)}
|
||||
@@ -289,7 +289,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-sm flex-1 min-w-0"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-sm flex-1 min-w-0"
|
||||
type="number"
|
||||
value={xpSpent}
|
||||
onChange={e => setXpSpent(e.target.value)}
|
||||
@@ -316,7 +316,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<select
|
||||
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-sm flex-1 min-w-0"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-sm flex-1 min-w-0"
|
||||
value={renown}
|
||||
onChange={e => setRenown(e.target.value)}
|
||||
>
|
||||
@@ -341,7 +341,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white placeholder-white/50 text-sm flex-1 min-w-0"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white placeholder-slate-400 text-sm flex-1 min-w-0"
|
||||
type="password"
|
||||
placeholder="New password (default: 1234)"
|
||||
value={pw}
|
||||
@@ -367,13 +367,13 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-xs w-16"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-xs w-16"
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={e => setAmount(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="text-xs px-3 py-1 rounded bg-purple-600/80 hover:bg-purple-600 text-white transition-colors whitespace-nowrap"
|
||||
className="text-xs px-3 py-1 rounded bg-purple-600 hover:bg-purple-600 text-white transition-colors whitespace-nowrap"
|
||||
onClick={() => {
|
||||
if (window.confirm(`Give ${amount} XP to all ${players.length} players?`)) {
|
||||
onGive(parseInt(amount));
|
||||
@@ -394,13 +394,13 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-xs w-16"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-xs w-16"
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={e => setAmount(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="text-xs px-3 py-1 rounded bg-purple-500/80 hover:bg-purple-500 text-white transition-colors whitespace-nowrap"
|
||||
className="text-xs px-3 py-1 rounded bg-purple-500 hover:bg-purple-500 text-white transition-colors whitespace-nowrap"
|
||||
onClick={() => {
|
||||
if (window.confirm(`Set all ${players.length} players to ${amount} XP?`)) {
|
||||
onSet(parseInt(amount));
|
||||
@@ -421,13 +421,13 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-xs w-16"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-xs w-16"
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={e => setAmount(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="text-xs px-3 py-1 rounded bg-blue-600/80 hover:bg-blue-600 text-white transition-colors whitespace-nowrap"
|
||||
className="text-xs px-3 py-1 rounded bg-blue-600 hover:bg-blue-600 text-white transition-colors whitespace-nowrap"
|
||||
onClick={() => {
|
||||
if (window.confirm(`Give ${amount} RP to all ${players.length} players?`)) {
|
||||
onGive(parseInt(amount));
|
||||
@@ -448,13 +448,13 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-xs w-16"
|
||||
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-xs w-16"
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={e => setAmount(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="text-xs px-3 py-1 rounded bg-blue-500/80 hover:bg-blue-500 text-white transition-colors whitespace-nowrap"
|
||||
className="text-xs px-3 py-1 rounded bg-blue-500 hover:bg-blue-500 text-white transition-colors whitespace-nowrap"
|
||||
onClick={() => {
|
||||
if (window.confirm(`Set all ${players.length} players to ${amount} RP?`)) {
|
||||
onSet(parseInt(amount));
|
||||
@@ -469,12 +469,12 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-white">Player Management</h2>
|
||||
<p className="text-sm text-slate-300 mb-6">Manage player accounts, requisition points, experience, and renown.</p>
|
||||
|
||||
{saveMsg && (
|
||||
<div className="mb-4 p-3 rounded bg-green-500/20 border border-green-500/30 text-green-300 text-sm">
|
||||
<div className="mb-4 p-3 rounded bg-green-800 border border-green-700 text-green-300 text-sm">
|
||||
{saveMsg}
|
||||
</div>
|
||||
)}
|
||||
@@ -493,7 +493,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{players.map(player => (
|
||||
<div key={player.name} className="bg-white/3 rounded-lg p-4 border border-white/5">
|
||||
<div key={player.name} className="bg-slate-800 rounded-lg p-4 border border-slate-600">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
{/* Player Info Section */}
|
||||
<div className="space-y-4">
|
||||
@@ -522,7 +522,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
<span className="text-slate-400">Available XP:</span> <span className="text-green-400 font-medium">{(player.tabInfo?.xp || 0) - (player.tabInfo?.xpSpent || 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-800/50 rounded p-3 border border-slate-700">
|
||||
<div className="bg-slate-800 rounded p-3 border border-slate-700">
|
||||
<div className="text-xs font-medium text-slate-300 uppercase tracking-wide mb-2">Experience Progress</div>
|
||||
<XPBar
|
||||
currentXP={player.tabInfo?.xp || 0}
|
||||
@@ -594,7 +594,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="mt-6 p-4 bg-white/3 rounded-lg border border-white/5">
|
||||
<div className="mt-6 p-4 bg-slate-800 rounded-lg border border-slate-600">
|
||||
<h4 className="text-sm font-medium text-white mb-3">Quick Actions</h4>
|
||||
|
||||
{/* Bulk XP Actions */}
|
||||
@@ -638,13 +638,13 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
|
||||
<h5 className="text-xs font-medium text-slate-400 uppercase tracking-wide mb-2">Other Actions</h5>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
className="text-xs px-3 py-1 rounded bg-blue-600/80 hover:bg-blue-600 text-white transition-colors"
|
||||
className="text-xs px-3 py-1 rounded bg-blue-600 hover:bg-blue-600 text-white transition-colors"
|
||||
onClick={fetchPlayers}
|
||||
>
|
||||
Refresh Players
|
||||
</button>
|
||||
<button
|
||||
className="text-xs px-3 py-1 rounded bg-green-600/80 hover:bg-green-600 text-white transition-colors"
|
||||
className="text-xs px-3 py-1 rounded bg-green-600 hover:bg-green-600 text-white transition-colors"
|
||||
onClick={() => {
|
||||
players.forEach(player => {
|
||||
if ((player.tabInfo?.rp || 0) < 10) {
|
||||
|
||||
@@ -529,14 +529,14 @@ function PlayerTab({
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-5 gap-2">
|
||||
<div className="flex gap-2">
|
||||
<input className="rounded-xl border border-white/10 bg-white/10 px-3 py-2 flex-1" placeholder="Player name" value={name} onChange={e=>setName(e.target.value)} />
|
||||
<select className="rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={usePregen ? name : ''} onChange={e=>{ setUsePregen(!!e.target.value); setName(e.target.value); }}>
|
||||
<input className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2 flex-1" placeholder="Player name" value={name} onChange={e=>setName(e.target.value)} />
|
||||
<select className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={usePregen ? name : ''} onChange={e=>{ setUsePregen(!!e.target.value); setName(e.target.value); }}>
|
||||
<option value="">-- pregens --</option>
|
||||
{pregens.map(p => <option key={p} value={p}>{p}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<input className="rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={rp} onChange={e=>setRp(parseInt(e.target.value||'0'))} />
|
||||
<input className="rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="password" placeholder="Password" value={pw} onChange={e=>setPw(e.target.value)} />
|
||||
<input className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={rp} onChange={e=>setRp(parseInt(e.target.value||'0'))} />
|
||||
<input className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="password" placeholder="Password" value={pw} onChange={e=>setPw(e.target.value)} />
|
||||
<button onClick={()=>{ onAdd(name, rp, pw); setName(''); setRp(10); setPw('') }} className="rounded-xl px-3 py-2 bg-amber-600 hover:bg-amber-500">Add/Update</button>
|
||||
<div className="self-center text-xs opacity-70">Add or overwrite by name</div>
|
||||
</div>
|
||||
@@ -546,7 +546,7 @@ function PlayerTab({
|
||||
const [rp, setRp] = useState('')
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<input className="rounded-xl border border-white/10 bg-white/10 px-2 py-1 text-sm w-20" type="number" placeholder="RP" value={rp} onChange={e=>setRp(e.target.value)} />
|
||||
<input className="rounded-xl border border-slate-600 bg-slate-800 px-2 py-1 text-sm w-20" type="number" placeholder="RP" value={rp} onChange={e=>setRp(e.target.value)} />
|
||||
<button onClick={()=>{ onSet(name, parseInt(rp||'0')); setRp('') }} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">Set RP</button>
|
||||
</div>
|
||||
)
|
||||
@@ -556,7 +556,7 @@ function PlayerTab({
|
||||
const [xp, setXp] = useState('')
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<input className="rounded-xl border border-white/10 bg-white/10 px-2 py-1 text-sm w-20" type="number" placeholder="XP" value={xp} onChange={e=>setXp(e.target.value)} />
|
||||
<input className="rounded-xl border border-slate-600 bg-slate-800 px-2 py-1 text-sm w-20" type="number" placeholder="XP" value={xp} onChange={e=>setXp(e.target.value)} />
|
||||
<button onClick={()=>{ onSet(name, parseInt(xp||'0')); setXp('') }} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">Set XP</button>
|
||||
</div>
|
||||
)
|
||||
@@ -566,7 +566,7 @@ function PlayerTab({
|
||||
const [xpSpent, setXpSpent] = useState('')
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<input className="rounded-xl border border-white/10 bg-white/10 px-2 py-1 text-sm w-20" type="number" placeholder="XP Spent" value={xpSpent} onChange={e=>setXpSpent(e.target.value)} />
|
||||
<input className="rounded-xl border border-slate-600 bg-slate-800 px-2 py-1 text-sm w-20" type="number" placeholder="XP Spent" value={xpSpent} onChange={e=>setXpSpent(e.target.value)} />
|
||||
<button onClick={()=>{ onSet(name, parseInt(xpSpent||'0')); setXpSpent('') }} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">Set XP Spent</button>
|
||||
</div>
|
||||
)
|
||||
@@ -576,7 +576,7 @@ function PlayerTab({
|
||||
const [renown, setRenown] = useState(value)
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<select className="rounded-xl border border-white/10 bg-white/10 px-2 py-1 text-sm w-20" value={renown} onChange={e=>setRenown(e.target.value)}>
|
||||
<select className="rounded-xl border border-slate-600 bg-slate-800 px-2 py-1 text-sm w-20" value={renown} onChange={e=>setRenown(e.target.value)}>
|
||||
{RANK_ORDER.map(r => <option key={r} value={r}>{r}</option>)}
|
||||
</select>
|
||||
<button onClick={()=>{ onSet(name, renown); setRenown(value) }} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">Set Renown</button>
|
||||
@@ -587,7 +587,7 @@ function PlayerTab({
|
||||
const [pw, setPw] = useState('')
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<input className="rounded-xl border border-white/10 bg-white/10 px-2 py-1 text-sm" type="password" placeholder="New PW" value={pw} onChange={e=>setPw(e.target.value)} />
|
||||
<input className="rounded-xl border border-slate-600 bg-slate-800 px-2 py-1 text-sm" type="password" placeholder="New PW" value={pw} onChange={e=>setPw(e.target.value)} />
|
||||
<button onClick={()=>{ onReset(name, pw); setPw('') }} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">Reset PW</button>
|
||||
</div>
|
||||
)
|
||||
@@ -768,9 +768,9 @@ function PlayerTab({
|
||||
<div className="mx-auto max-w-7xl space-y-6">
|
||||
|
||||
{/* Avatar header (standalone) */}
|
||||
<div className="flex flex-col items-center bg-white/5 rounded-xl p-4 border border-white/10">
|
||||
<div className="flex flex-col items-center bg-slate-800 rounded-xl p-4 border border-slate-600">
|
||||
<div className="w-full flex justify-center">
|
||||
<img src={picture || '/logo192.png'} alt="avatar" className="w-36 h-36 md:w-48 md:h-48 rounded-full object-cover border-4 border-white/10 shadow-lg" />
|
||||
<img src={picture || '/logo192.png'} alt="avatar" className="w-36 h-36 md:w-48 md:h-48 rounded-full object-cover border-4 border-slate-600 shadow-lg" />
|
||||
</div>
|
||||
<div className="mt-3 text-center">
|
||||
<div className="text-lg font-semibold">{charName || currentPlayer?.tabInfo?.charName || 'Unknown'}</div>
|
||||
@@ -781,7 +781,7 @@ function PlayerTab({
|
||||
<div className="flex flex-col items-center">
|
||||
<input type="file" accept="image/*" onChange={e=>uploadAvatarFile(e.target.files?.[0])} />
|
||||
<div className="mt-1">Max 200KB. Supported: png/jpg/gif</div>
|
||||
<input className="w-64 md:w-96 mx-auto mt-2 rounded border border-white/10 bg-white/10 px-2 py-1" value={picture} onChange={e=>setPicture(e.target.value)} placeholder="Or paste image URL" />
|
||||
<input className="w-64 md:w-96 mx-auto mt-2 rounded border border-slate-600 bg-slate-800 px-2 py-1" value={picture} onChange={e=>setPicture(e.target.value)} placeholder="Or paste image URL" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -794,14 +794,14 @@ function PlayerTab({
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={saveLocal}
|
||||
className={`px-3 py-1.5 rounded-lg border border-white/10 text-sm ${(authedPlayer || gmOpen) ? 'bg-emerald-600 hover:bg-emerald-500' : 'bg-emerald-900/40 cursor-not-allowed'}`}
|
||||
className={`px-3 py-1.5 rounded-lg border border-slate-600 text-sm ${(authedPlayer || gmOpen) ? 'bg-emerald-600 hover:bg-emerald-500' : 'bg-emerald-800 cursor-not-allowed'}`}
|
||||
disabled={!(authedPlayer || gmOpen)}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button onClick={() => setShowLogs(s => !s)} className="px-3 py-1.5 rounded-lg border border-white/10 text-sm bg-slate-700 hover:bg-slate-600">{showLogs ? 'Hide Logs' : 'Show Logs'}</button>
|
||||
<button onClick={() => setShowLogs(s => !s)} className="px-3 py-1.5 rounded-lg border border-slate-600 text-sm bg-slate-700 hover:bg-slate-600">{showLogs ? 'Hide Logs' : 'Show Logs'}</button>
|
||||
{saveMsg && (
|
||||
<span className="ml-2 text-xs px-2 py-1 rounded bg-white/10 border border-white/10">
|
||||
<span className="ml-2 text-xs px-2 py-1 rounded bg-slate-800 border border-slate-600">
|
||||
{saveMsg}
|
||||
</span>
|
||||
)}
|
||||
@@ -810,7 +810,7 @@ function PlayerTab({
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Chapter</label>
|
||||
<input
|
||||
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
|
||||
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
|
||||
value={chapter}
|
||||
onChange={(e) => setChapter(e.target.value)}
|
||||
disabled={!(shopAuthed || gmOpen)}
|
||||
@@ -819,7 +819,7 @@ function PlayerTab({
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Chapter Demeanour</label>
|
||||
<input
|
||||
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
|
||||
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
|
||||
value={demeanour}
|
||||
onChange={(e) => setDemeanour(e.target.value)}
|
||||
disabled={!(shopAuthed || gmOpen)}
|
||||
@@ -828,7 +828,7 @@ function PlayerTab({
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Speciality</label>
|
||||
<input
|
||||
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
|
||||
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
|
||||
value={speciality}
|
||||
onChange={(e) => setSpeciality(e.target.value)}
|
||||
disabled={!(shopAuthed || gmOpen)}
|
||||
@@ -837,7 +837,7 @@ function PlayerTab({
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Rank</label>
|
||||
<input
|
||||
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
|
||||
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
|
||||
value={rank}
|
||||
onChange={(e) => setRank(e.target.value)}
|
||||
disabled={!(shopAuthed || gmOpen)}
|
||||
@@ -846,7 +846,7 @@ function PlayerTab({
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Power Armour History</label>
|
||||
<input
|
||||
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
|
||||
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
|
||||
value={powerArmour}
|
||||
onChange={(e) => setPowerArmour(e.target.value)}
|
||||
disabled={!(shopAuthed || gmOpen)}
|
||||
@@ -855,7 +855,7 @@ function PlayerTab({
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Description</label>
|
||||
<input
|
||||
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
|
||||
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
disabled={!(shopAuthed || gmOpen)}
|
||||
@@ -864,7 +864,7 @@ function PlayerTab({
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Past Event</label>
|
||||
<input
|
||||
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
|
||||
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
|
||||
value={pastEvent}
|
||||
onChange={(e) => setPastEvent(e.target.value)}
|
||||
disabled={!(shopAuthed || gmOpen)}
|
||||
@@ -873,7 +873,7 @@ function PlayerTab({
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Personal Demeanour</label>
|
||||
<input
|
||||
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
|
||||
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
|
||||
value={personalDemeanour}
|
||||
onChange={(e) => setPersonalDemeanour(e.target.value)}
|
||||
disabled={!(shopAuthed || gmOpen)}
|
||||
@@ -884,7 +884,7 @@ function PlayerTab({
|
||||
<div className="col-span-4 flex justify-end mt-4">
|
||||
<button
|
||||
onClick={() => setGmOpen(!gmOpen)}
|
||||
className="px-3 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-500 border border-white/10 text-sm"
|
||||
className="px-3 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-500 border border-slate-600 text-sm"
|
||||
>
|
||||
{gmOpen ? 'Close GM Panel' : 'Open GM Panel'}
|
||||
</button>
|
||||
@@ -893,7 +893,7 @@ function PlayerTab({
|
||||
</div>
|
||||
|
||||
{/* Requisition Points Display */}
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 p-3 space-y-2">
|
||||
<div className="rounded-xl border border-slate-600 bg-slate-900 p-3 space-y-2">
|
||||
<div className="text-lg font-medium">Requisition Points</div>
|
||||
<div className="text-base">
|
||||
Current RP: <span className="font-semibold text-xl text-amber-400">{currentPlayer?.tabInfo?.rp || '0'}</span>
|
||||
@@ -904,7 +904,7 @@ function PlayerTab({
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
className="w-24 rounded border border-white/10 bg-white/10 px-2 py-1"
|
||||
className="w-24 rounded border border-slate-600 bg-slate-800 px-2 py-1"
|
||||
placeholder="RP"
|
||||
defaultValue={currentPlayer?.tabInfo?.rp || 0}
|
||||
onChange={(e) => {
|
||||
@@ -938,7 +938,7 @@ function PlayerTab({
|
||||
</div>
|
||||
|
||||
{/* Experience Points Display - Minecraft-style XP Bar */}
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 p-3 space-y-2">
|
||||
<div className="rounded-xl border border-slate-600 bg-slate-900 p-3 space-y-2">
|
||||
<div className="text-lg font-medium">Experience Points (XP)</div>
|
||||
|
||||
{/* XP Bar */}
|
||||
@@ -966,7 +966,7 @@ function PlayerTab({
|
||||
|
||||
{/* GM Controls */}
|
||||
{isGMLoggedIn() && (
|
||||
<div className="mt-4 space-y-2 border-t border-white/10 pt-3">
|
||||
<div className="mt-4 space-y-2 border-t border-slate-600 pt-3">
|
||||
<div className="text-sm text-slate-300 font-medium">GM Controls</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -1014,20 +1014,20 @@ function PlayerTab({
|
||||
</div>
|
||||
|
||||
{/* Characteristics */}
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
|
||||
<div className="font-semibold mb-2">Characteristics</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-9 gap-2">
|
||||
{CHARACTERISTICS.map(c => (
|
||||
<div key={c.key} className="flex flex-col items-center">
|
||||
<label className="text-xs opacity-70 mb-1">{c.label}</label>
|
||||
<input type="number" className="w-16 text-center text-lg rounded border border-white/10 bg-white/10 px-2 py-1" value={characteristics[c.key]} onChange={e=>handleCharChange(c.key, parseInt(e.target.value||'0'))} />
|
||||
<input type="number" className="w-16 text-center text-lg rounded border border-slate-600 bg-slate-800 px-2 py-1" value={characteristics[c.key]} onChange={e=>handleCharChange(c.key, parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills */}
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
|
||||
<div className="font-semibold mb-2">Skills</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{SKILLS.map(skill => (
|
||||
@@ -1042,11 +1042,11 @@ function PlayerTab({
|
||||
</div>
|
||||
|
||||
{/* Space Marine Abilities - hover tooltips */}
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
|
||||
<div className="font-semibold mb-2">Space Marine Abilities</div>
|
||||
<div className="text-xs opacity-80 grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{SPACE_MARINE_ABILITIES.map(a => (
|
||||
<div key={a.name} className="p-2 bg-white/3 rounded flex items-center justify-between">
|
||||
<div key={a.name} className="p-2 bg-slate-800 rounded flex items-center justify-between">
|
||||
<span className="mr-2">{a.name}</span>
|
||||
<Tooltip text={a.desc}><span className="inline-block w-5 h-5 text-center text-black bg-white rounded-full text-xs leading-5">?</span></Tooltip>
|
||||
</div>
|
||||
@@ -1055,11 +1055,11 @@ function PlayerTab({
|
||||
</div>
|
||||
|
||||
{/* Power Armour Abilities - hover tooltips */}
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10 mt-4">
|
||||
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600 mt-4">
|
||||
<div className="font-semibold mb-2">Power Armour Abilities (standard)</div>
|
||||
<div className="text-xs opacity-80 grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{POWER_ARMOUR_ABILITIES.map(a => (
|
||||
<div key={a.name} className="p-2 bg-white/3 rounded flex items-center justify-between">
|
||||
<div key={a.name} className="p-2 bg-slate-800 rounded flex items-center justify-between">
|
||||
<span className="mr-2">{a.name}</span>
|
||||
<Tooltip text={a.desc}><span className="inline-block w-5 h-5 text-center text-black bg-white rounded-full text-xs leading-5">?</span></Tooltip>
|
||||
</div>
|
||||
@@ -1068,7 +1068,7 @@ function PlayerTab({
|
||||
</div>
|
||||
|
||||
{/* Gear Section */}
|
||||
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
|
||||
<div className="font-semibold text-lg mb-4 flex items-center justify-between">
|
||||
<span>Assigned Gear</span>
|
||||
{isGMOrShopAuthed() && (
|
||||
@@ -1082,7 +1082,7 @@ function PlayerTab({
|
||||
</div>
|
||||
<div className="space-y-4 max-h-[600px] overflow-y-auto pr-4">
|
||||
{/* Column Headers */}
|
||||
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-slate-400 pb-2 border-b border-white/10">
|
||||
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-slate-400 pb-2 border-b border-slate-600">
|
||||
<div className="col-span-5">Item Name</div>
|
||||
<div className="col-span-2">Quantity</div>
|
||||
<div className="col-span-3">Notes</div>
|
||||
@@ -1091,9 +1091,9 @@ function PlayerTab({
|
||||
|
||||
{/* Gear Items */}
|
||||
{gear.map((item, index) => (
|
||||
<div key={index} className="grid grid-cols-12 gap-4 items-center bg-white/5 rounded-lg p-2">
|
||||
<div key={index} className="grid grid-cols-12 gap-4 items-center bg-slate-800 rounded-lg p-2">
|
||||
<input
|
||||
className="col-span-5 rounded border border-white/10 bg-white/10 px-3 py-2"
|
||||
className="col-span-5 rounded border border-slate-600 bg-slate-800 px-3 py-2"
|
||||
placeholder="Item name"
|
||||
value={item.name || ''}
|
||||
onChange={(e) => updateGear(item.id, { name: e.target.value })}
|
||||
@@ -1101,7 +1101,7 @@ function PlayerTab({
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="col-span-2 rounded border border-white/10 bg-white/10 px-3 py-2"
|
||||
className="col-span-2 rounded border border-slate-600 bg-slate-800 px-3 py-2"
|
||||
placeholder="Qty"
|
||||
value={item.qty || 1}
|
||||
onChange={(e) => updateGear(item.id, { qty: parseInt(e.target.value) || 1 })}
|
||||
@@ -1109,7 +1109,7 @@ function PlayerTab({
|
||||
min="1"
|
||||
/>
|
||||
<input
|
||||
className="col-span-3 rounded border border-white/10 bg-white/10 px-3 py-2"
|
||||
className="col-span-3 rounded border border-slate-600 bg-slate-800 px-3 py-2"
|
||||
placeholder="Add notes..."
|
||||
value={item.note || ''}
|
||||
onChange={(e) => updateGear(item.id, { note: e.target.value })}
|
||||
@@ -1162,89 +1162,89 @@ function PlayerTab({
|
||||
|
||||
{/* Weapons & Armour */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
|
||||
<div className="font-semibold mb-2">Weapons</div>
|
||||
{[0,1,2].map(idx => (
|
||||
<div key={idx} className="mb-2 border-b border-white/10 pb-2">
|
||||
<div key={idx} className="mb-2 border-b border-slate-600 pb-2">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-1">
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Name" value={weapons[idx]?.name||''} onChange={e=>handleWeaponChange(idx,'name',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Class" value={weapons[idx]?.class||''} onChange={e=>handleWeaponChange(idx,'class',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Damage" value={weapons[idx]?.damage||''} onChange={e=>handleWeaponChange(idx,'damage',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Type" value={weapons[idx]?.type||''} onChange={e=>handleWeaponChange(idx,'type',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Name" value={weapons[idx]?.name||''} onChange={e=>handleWeaponChange(idx,'name',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Class" value={weapons[idx]?.class||''} onChange={e=>handleWeaponChange(idx,'class',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Damage" value={weapons[idx]?.damage||''} onChange={e=>handleWeaponChange(idx,'damage',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Type" value={weapons[idx]?.type||''} onChange={e=>handleWeaponChange(idx,'type',e.target.value)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-1">
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Pen" value={weapons[idx]?.pen||''} onChange={e=>handleWeaponChange(idx,'pen',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Range" value={weapons[idx]?.range||''} onChange={e=>handleWeaponChange(idx,'range',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="RoF" value={weapons[idx]?.rof||''} onChange={e=>handleWeaponChange(idx,'rof',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Clip" value={weapons[idx]?.clip||''} onChange={e=>handleWeaponChange(idx,'clip',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Pen" value={weapons[idx]?.pen||''} onChange={e=>handleWeaponChange(idx,'pen',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Range" value={weapons[idx]?.range||''} onChange={e=>handleWeaponChange(idx,'range',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="RoF" value={weapons[idx]?.rof||''} onChange={e=>handleWeaponChange(idx,'rof',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Clip" value={weapons[idx]?.clip||''} onChange={e=>handleWeaponChange(idx,'clip',e.target.value)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-1">
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Rld" value={weapons[idx]?.rld||''} onChange={e=>handleWeaponChange(idx,'rld',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Special Rules" value={weapons[idx]?.special||''} onChange={e=>handleWeaponChange(idx,'special',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Rld" value={weapons[idx]?.rld||''} onChange={e=>handleWeaponChange(idx,'rld',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Special Rules" value={weapons[idx]?.special||''} onChange={e=>handleWeaponChange(idx,'special',e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
|
||||
<div className="font-semibold mb-2">Armour</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Head" value={armour.head} onChange={e=>handleArmourChange('head',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Body" value={armour.body} onChange={e=>handleArmourChange('body',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Right Arm" value={armour.ra} onChange={e=>handleArmourChange('ra',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Left Arm" value={armour.la} onChange={e=>handleArmourChange('la',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Right Leg" value={armour.rl} onChange={e=>handleArmourChange('rl',e.target.value)} />
|
||||
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Left Leg" value={armour.ll} onChange={e=>handleArmourChange('ll',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Head" value={armour.head} onChange={e=>handleArmourChange('head',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Body" value={armour.body} onChange={e=>handleArmourChange('body',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Right Arm" value={armour.ra} onChange={e=>handleArmourChange('ra',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Left Arm" value={armour.la} onChange={e=>handleArmourChange('la',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Right Leg" value={armour.rl} onChange={e=>handleArmourChange('rl',e.target.value)} />
|
||||
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Left Leg" value={armour.ll} onChange={e=>handleArmourChange('ll',e.target.value)} />
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Additions/Notes" value={armour.additions} onChange={e=>handleArmourChange('additions',e.target.value)} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Additions/Notes" value={armour.additions} onChange={e=>handleArmourChange('additions',e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Talents, Psychic Powers */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
|
||||
<div className="font-semibold mb-2">Talents & Traits</div>
|
||||
<textarea className="w-full h-32 rounded border border-white/10 bg-white/10 px-2 py-1" value={talents} onChange={e=>setTalents(e.target.value)} />
|
||||
<textarea className="w-full h-32 rounded border border-slate-600 bg-slate-800 px-2 py-1" value={talents} onChange={e=>setTalents(e.target.value)} />
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
|
||||
<div className="font-semibold mb-2">Psychic Powers</div>
|
||||
<textarea className="w-full h-32 rounded border border-white/10 bg-white/10 px-2 py-1" value={psychic} onChange={e=>setPsychic(e.target.value)} />
|
||||
<textarea className="w-full h-32 rounded border border-slate-600 bg-slate-800 px-2 py-1" value={psychic} onChange={e=>setPsychic(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wounds, Insanity, Movement, Fate, Corruption, Renown, XP */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-2 bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-2 bg-slate-800 rounded-xl p-3 border border-slate-600">
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Wounds</label>
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Total" value={wounds.total} onChange={e=>setWounds(w=>({...w,total:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Current" value={wounds.current} onChange={e=>setWounds(w=>({...w,current:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" placeholder="Fatigue" value={wounds.fatigue} onChange={e=>setWounds(w=>({...w,fatigue:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Total" value={wounds.total} onChange={e=>setWounds(w=>({...w,total:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Current" value={wounds.current} onChange={e=>setWounds(w=>({...w,current:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" placeholder="Fatigue" value={wounds.fatigue} onChange={e=>setWounds(w=>({...w,fatigue:parseInt(e.target.value||'0')}))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Insanity</label>
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Current" value={insanity.current} onChange={e=>setInsanity(i=>({...i,current:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Battle Fatigue" value={insanity.battleFatigue} onChange={e=>setInsanity(i=>({...i,battleFatigue:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" placeholder="Primarch's Curse" value={insanity.primarchsCurse} onChange={e=>setInsanity(i=>({...i,primarchsCurse:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Current" value={insanity.current} onChange={e=>setInsanity(i=>({...i,current:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Battle Fatigue" value={insanity.battleFatigue} onChange={e=>setInsanity(i=>({...i,battleFatigue:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" placeholder="Primarch's Curse" value={insanity.primarchsCurse} onChange={e=>setInsanity(i=>({...i,primarchsCurse:parseInt(e.target.value||'0')}))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Movement</label>
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Half" value={movement.half} onChange={e=>setMovement(m=>({...m,half:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Charge" value={movement.charge} onChange={e=>setMovement(m=>({...m,charge:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" placeholder="Full" value={movement.full} onChange={e=>setMovement(m=>({...m,full:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Half" value={movement.half} onChange={e=>setMovement(m=>({...m,half:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Charge" value={movement.charge} onChange={e=>setMovement(m=>({...m,charge:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" placeholder="Full" value={movement.full} onChange={e=>setMovement(m=>({...m,full:parseInt(e.target.value||'0')}))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Fate</label>
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Total" value={fate.total} onChange={e=>setFate(f=>({...f,total:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" placeholder="Current" value={fate.current} onChange={e=>setFate(f=>({...f,current:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Total" value={fate.total} onChange={e=>setFate(f=>({...f,total:parseInt(e.target.value||'0')}))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" placeholder="Current" value={fate.current} onChange={e=>setFate(f=>({...f,current:parseInt(e.target.value||'0')}))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Corruption</label>
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" placeholder="Current" value={corruption} onChange={e=>setCorruption(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" placeholder="Current" value={corruption} onChange={e=>setCorruption(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Renown</label>
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" value={renown} onChange={e=>setRenown(e.target.value)} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" value={renown} onChange={e=>setRenown(e.target.value)} />
|
||||
<label className="text-xs uppercase opacity-70 mt-3 block">Experience Points</label>
|
||||
<div className="mb-3">
|
||||
<XPBar currentXP={xp} xpSpent={xpSpent} thresholdXP={500} showLabel={true} compact={false} />
|
||||
@@ -1252,25 +1252,25 @@ function PlayerTab({
|
||||
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
|
||||
<div>
|
||||
<label className="uppercase opacity-70 block">XP Total</label>
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" value={xp} onChange={e=>setXp(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" value={xp} onChange={e=>setXp(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="uppercase opacity-70 block">XP Spent</label>
|
||||
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" value={xpSpent} onChange={e=>setXpSpent(parseInt(e.target.value||'0'))} />
|
||||
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" value={xpSpent} onChange={e=>setXpSpent(parseInt(e.target.value||'0'))} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
|
||||
<div className="font-semibold mb-2">Notes</div>
|
||||
<textarea className="w-full h-24 rounded border border-white/10 bg-white/10 px-2 py-1" value={notes} onChange={e=>setNotes(e.target.value)} />
|
||||
<textarea className="w-full h-24 rounded border border-slate-600 bg-slate-800 px-2 py-1" value={notes} onChange={e=>setNotes(e.target.value)} />
|
||||
</div>
|
||||
|
||||
{/* GM Panel - Player Management */}
|
||||
{isGMLoggedIn() && (
|
||||
<div className="bg-white/5 rounded-xl p-4 border border-white/10 mt-4">
|
||||
<div className="bg-slate-800 rounded-xl p-4 border border-slate-600 mt-4">
|
||||
<div className="font-semibold text-lg mb-4">Player Management</div>
|
||||
|
||||
{/* Add/Update Player */}
|
||||
@@ -1349,7 +1349,7 @@ function PlayerTab({
|
||||
|
||||
{/* In-app log panel */}
|
||||
{showLogs && (
|
||||
<div className="mt-4 bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="mt-4 bg-slate-800 rounded-xl p-3 border border-slate-600">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="font-semibold">Client Logs (latest)</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -1360,7 +1360,7 @@ function PlayerTab({
|
||||
<div className="max-h-64 overflow-y-auto text-xs font-mono">
|
||||
{logs.length === 0 && <div className="opacity-70">No logs</div>}
|
||||
{logs.map(l => (
|
||||
<div key={l.id} className="mb-1 border-b border-white/5 pb-1">
|
||||
<div key={l.id} className="mb-1 border-b border-slate-600 pb-1">
|
||||
<div className="text-xs opacity-80">{l.timestamp} <span className="uppercase">{l.level}</span> <span className="opacity-60">[{l.component}]</span></div>
|
||||
<div className="text-sm">{l.message}</div>
|
||||
{l.data && <pre className="text-xs mt-1 whitespace-pre-wrap">{l.data}</pre>}
|
||||
|
||||
@@ -234,23 +234,23 @@ export default function RequisitionShop({ authedPlayer, sessionId }) {
|
||||
}, [errorMsg]);
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl bg-white/5 backdrop-blur border border-white/10 p-5 shadow-xl space-y-4">
|
||||
<div className="rounded-2xl bg-slate-800 border border-slate-700 p-5 shadow-xl space-y-4">
|
||||
<div className="text-2xl font-bold">Requisition Shop</div>
|
||||
|
||||
{errorMsg && (
|
||||
<div className="p-3 rounded-lg bg-red-900/20 border border-red-500/30">
|
||||
<div className="p-3 rounded-lg bg-red-800 border border-red-600">
|
||||
<p className="text-red-300">{errorMsg}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!authedPlayer ? (
|
||||
<div className="p-4 rounded-lg bg-amber-900/20 border border-amber-500/30">
|
||||
<div className="p-4 rounded-lg bg-amber-800 border border-amber-600">
|
||||
<p className="text-amber-300">Please log in using the header above to access the shop.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Player Info */}
|
||||
<div className="p-3 rounded-lg bg-blue-900/20 border border-blue-500/30">
|
||||
<div className="p-3 rounded-lg bg-blue-800 border border-blue-600">
|
||||
<div className="text-lg font-semibold text-blue-300">
|
||||
Current Player: {authedPlayer}
|
||||
</div>
|
||||
@@ -264,13 +264,13 @@ export default function RequisitionShop({ authedPlayer, sessionId }) {
|
||||
{/* Search and Filter */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<input
|
||||
className="rounded-xl border border-white/10 bg-white/10 px-3 py-2"
|
||||
className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2"
|
||||
placeholder="Search items..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="rounded-xl border border-white/10 bg-white/10 px-3 py-2"
|
||||
className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2"
|
||||
value={categoryFilter}
|
||||
onChange={e => setCategoryFilter(e.target.value)}
|
||||
>
|
||||
@@ -283,7 +283,7 @@ export default function RequisitionShop({ authedPlayer, sessionId }) {
|
||||
{/* Items Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredItems.map(item => (
|
||||
<div key={item.id} className="p-3 rounded-lg bg-slate-800/50 border border-white/10">
|
||||
<div key={item.id} className="p-3 rounded-lg bg-slate-800 border border-slate-700">
|
||||
<div className="font-semibold text-white">{item.name}</div>
|
||||
<div className="text-xs text-slate-300 mb-2">{item.category}</div>
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
</div>
|
||||
|
||||
{/* Search Section */}
|
||||
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
|
||||
<form onSubmit={handleSearch} className="space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
@@ -183,13 +183,13 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search for rules, weapons, talents, skills..."
|
||||
className="w-full px-4 py-2 rounded-lg border border-white/20 bg-white/10 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full px-4 py-2 rounded-lg border border-slate-600 bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => handleCategoryChange(e.target.value)}
|
||||
className="px-4 py-2 rounded-lg border border-white/20 bg-white/10 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="px-4 py-2 rounded-lg border border-slate-600 bg-slate-800 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{ruleCategories.map(cat => (
|
||||
<option key={cat.id} value={cat.id} className="bg-slate-800">
|
||||
@@ -235,7 +235,7 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
<button
|
||||
key={rule}
|
||||
onClick={() => handleQuickSearch(rule)}
|
||||
className="px-3 py-1 text-sm bg-white/10 hover:bg-white/20 rounded-md border border-white/20 transition-colors"
|
||||
className="px-3 py-1 text-sm bg-slate-800 hover:bg-slate-700 rounded-md border border-slate-600 transition-colors"
|
||||
>
|
||||
{rule}
|
||||
</button>
|
||||
@@ -255,7 +255,7 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
setSelectedCategory(cid);
|
||||
await fetchRandom(cid, browseCount);
|
||||
}}
|
||||
className={`px-3 py-1 text-sm rounded-md border transition-colors ${selectedCategory === cat.id ? 'bg-blue-500 text-white border-blue-600' : 'bg-white/10 text-white border-white/20 hover:bg-white/20'}`}
|
||||
className={`px-3 py-1 text-sm rounded-md border transition-colors ${selectedCategory === cat.id ? 'bg-blue-500 text-white border-blue-600' : 'bg-slate-800 text-white border-slate-600 hover:bg-slate-700'}`}
|
||||
>
|
||||
{cat.name}
|
||||
</button>
|
||||
@@ -272,7 +272,7 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleQuickSearch(query)}
|
||||
className="px-2 py-1 text-xs bg-white/5 hover:bg-white/10 rounded border border-white/10 transition-colors"
|
||||
className="px-2 py-1 text-xs bg-slate-800 hover:bg-slate-800 rounded border border-slate-600 transition-colors"
|
||||
>
|
||||
{query}
|
||||
</button>
|
||||
@@ -284,7 +284,7 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
|
||||
{/* Search Results */}
|
||||
{searchResults.length > 0 && (
|
||||
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
Search Results ({searchResults.length})
|
||||
</h2>
|
||||
@@ -293,20 +293,20 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
{searchResults.map((rule, index) => (
|
||||
<div
|
||||
key={rule.id || index}
|
||||
className="bg-white/5 rounded-lg p-4 border border-white/10 hover:bg-white/10 transition-colors cursor-pointer"
|
||||
className="bg-slate-800 rounded-lg p-4 border border-slate-600 hover:bg-slate-800 transition-colors cursor-pointer"
|
||||
onClick={() => setSelectedRule(rule)}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-lg font-medium text-blue-300">{rule.title}</h3>
|
||||
<div className="flex gap-2 text-xs">
|
||||
{rule.category && (
|
||||
<span className="px-2 py-1 bg-blue-600/30 rounded">{rule.category}</span>
|
||||
<span className="px-2 py-1 bg-blue-700 rounded">{rule.category}</span>
|
||||
)}
|
||||
{rule.page && (
|
||||
<span className="px-2 py-1 bg-green-600/30 rounded">p.{rule.page}</span>
|
||||
<span className="px-2 py-1 bg-green-700 rounded">p.{rule.page}</span>
|
||||
)}
|
||||
{rule.source && (
|
||||
<span className="px-2 py-1 bg-purple-600/30 rounded">{rule.source}</span>
|
||||
<span className="px-2 py-1 bg-purple-700 rounded">{rule.source}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -339,8 +339,8 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
|
||||
{/* Rule Detail Modal */}
|
||||
{selectedRule && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-slate-800 rounded-xl p-6 max-w-4xl max-h-[80vh] overflow-y-auto border border-white/20">
|
||||
<div className="fixed inset-0 bg-black flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-slate-800 rounded-xl p-6 max-w-4xl max-h-[80vh] overflow-y-auto border border-slate-600">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h2 className="text-2xl font-bold text-blue-300">{selectedRule.title}</h2>
|
||||
<button
|
||||
@@ -353,13 +353,13 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
|
||||
<div className="flex gap-2 mb-4 text-xs">
|
||||
{selectedRule.category && (
|
||||
<span className="px-2 py-1 bg-blue-600/30 rounded">{selectedRule.category}</span>
|
||||
<span className="px-2 py-1 bg-blue-700 rounded">{selectedRule.category}</span>
|
||||
)}
|
||||
{selectedRule.page && (
|
||||
<span className="px-2 py-1 bg-green-600/30 rounded">Page {selectedRule.page}</span>
|
||||
<span className="px-2 py-1 bg-green-700 rounded">Page {selectedRule.page}</span>
|
||||
)}
|
||||
{selectedRule.source && (
|
||||
<span className="px-2 py-1 bg-purple-600/30 rounded">{selectedRule.source}</span>
|
||||
<span className="px-2 py-1 bg-purple-700 rounded">{selectedRule.source}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -371,7 +371,7 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
/>
|
||||
|
||||
{selectedRule.examples && (
|
||||
<div className="mt-4 p-4 bg-white/5 rounded-lg border border-white/10">
|
||||
<div className="mt-4 p-4 bg-slate-800 rounded-lg border border-slate-600">
|
||||
<h4 className="font-semibold mb-2 text-green-300">Examples:</h4>
|
||||
<div className="text-slate-300">{selectedRule.examples}</div>
|
||||
</div>
|
||||
@@ -385,7 +385,7 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleQuickSearch(related)}
|
||||
className="px-2 py-1 text-xs bg-white/10 hover:bg-white/20 rounded border border-white/20 transition-colors"
|
||||
className="px-2 py-1 text-xs bg-slate-800 hover:bg-slate-700 rounded border border-slate-600 transition-colors"
|
||||
>
|
||||
{related}
|
||||
</button>
|
||||
@@ -399,7 +399,7 @@ function RulesTab({ authedPlayer, sessionId }) {
|
||||
|
||||
{/* No Results */}
|
||||
{searchQuery && !loading && searchResults.length === 0 && (
|
||||
<div className="bg-white/5 rounded-xl p-6 border border-white/10 text-center">
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600 text-center">
|
||||
<p className="text-slate-400">No rules found for "{searchQuery}"</p>
|
||||
<p className="text-xs text-slate-500 mt-2">Try different keywords or check a different category</p>
|
||||
</div>
|
||||
|
||||
@@ -70,21 +70,21 @@ export function XPSummary({ currentXP = 0, xpSpent = 0, thresholdXP = 500 }) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="bg-slate-700/50 rounded p-2">
|
||||
<div className="text-xs text-slate-400">Total XP</div>
|
||||
<div className="bg-slate-700 rounded p-2 border border-slate-600">
|
||||
<div className="text-xs text-slate-300">Total XP</div>
|
||||
<div className="text-lg font-bold text-green-400">{currentXP}</div>
|
||||
</div>
|
||||
<div className="bg-slate-700/50 rounded p-2">
|
||||
<div className="text-xs text-slate-400">Available XP</div>
|
||||
<div className="bg-slate-700 rounded p-2 border border-slate-600">
|
||||
<div className="text-xs text-slate-300">Available XP</div>
|
||||
<div className="text-lg font-bold text-green-300">{availableXP}</div>
|
||||
</div>
|
||||
<div className="bg-slate-700/50 rounded p-2 col-span-2">
|
||||
<div className="text-xs text-slate-400">Current Level</div>
|
||||
<div className="text-lg font-bold text-blue-400">Level {currentLevel}</div>
|
||||
<div className="bg-slate-700 rounded p-2 col-span-2 border border-slate-600">
|
||||
<div className="text-xs text-slate-300">Current Level</div>
|
||||
<div className="text-lg font-bold text-blue-300">Level {currentLevel}</div>
|
||||
</div>
|
||||
</div>
|
||||
{xpSpent > 0 && (
|
||||
<div className="text-xs text-slate-400">
|
||||
<div className="text-xs text-slate-300">
|
||||
Spent: {xpSpent} XP | Remaining: {availableXP} XP
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply rounded-2xl bg-white/5 backdrop-blur border border-white/10 p-4 sm:p-5 shadow-xl;
|
||||
@apply rounded-2xl bg-slate-800 backdrop-blur border border-slate-600 p-4 sm:p-5 shadow-xl;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -48,11 +48,11 @@
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2;
|
||||
@apply w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2;
|
||||
}
|
||||
|
||||
.textarea-field {
|
||||
@apply w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2;
|
||||
@apply w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2;
|
||||
}
|
||||
|
||||
.select-field {
|
||||
@@ -76,7 +76,7 @@
|
||||
}
|
||||
|
||||
.roll-card {
|
||||
@apply rounded-xl border border-white/10 bg-black/20 p-3;
|
||||
@apply rounded-xl border border-slate-600 bg-slate-800 p-3;
|
||||
}
|
||||
|
||||
.roll-type {
|
||||
@@ -92,6 +92,6 @@
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
@apply h-2 w-full bg-white/10 rounded overflow-hidden;
|
||||
@apply h-2 w-full bg-slate-700 rounded overflow-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user