This commit is contained in:
Marces Zastrow
2025-01-17 08:10:15 +01:00
parent d0dd69e9b5
commit a3bf8064d9
7 changed files with 681 additions and 10 deletions
Binary file not shown.
+68 -7
View File
@@ -95,6 +95,21 @@ app.get('/games/:userId', (req, res) => {
}); });
// Fetch game details including game master
app.get('/games/:gameId/master', (req, res) => {
const gameId = req.params.gameId;
db.get('SELECT * FROM games WHERE game_id = ?', [gameId], (err, row) => {
if (err) {
return res.status(500).json({ error: 'Internal server error' });
}
if (!row) {
return res.status(404).json({ error: 'Game not found' });
}
// Convert game_master_id to number for consistent comparison
res.json(row);
});
});
// Join Game // Join Game
app.post('/joinGame/:gameId', (req, res) => { app.post('/joinGame/:gameId', (req, res) => {
const gameId = req.params.gameId; const gameId = req.params.gameId;
@@ -162,18 +177,25 @@ app.post('/createGame', (req, res) => {
// Fetch game Participant Characters // Fetch game Participant Characters
app.get('/games/:gameId/playerchars', (req, res) => { app.get('/games/:gameId/playerchars', (req, res) => {
const gameId = req.params.gameId; const gameId = req.params.gameId;
db.get('SELECT * FROM PlayerCharacter WHERE GameID = ?', [gameId], (err, row) => { db.all('SELECT * FROM PlayerCharacter WHERE GameID = ?', [gameId], (err, rows) => {
if (err) { if (err) {
return res.status(500).json({ error: 'Internal server error' }); return res.status(500).json({ error: 'Internal server error' });
} }
res.json(row); // Process each row and convert BLOB to base64 if image exists
const processedRows = rows.map(row => {
if (row.Img) {
row.Img = `data:image/jpeg;base64,${row.Img.toString('base64')}`;
}
return row;
});
res.json(processedRows);
});
}); });
})
// Fetch game NPCs // Fetch game NPCs
app.get('/games/:gameId/npcs', (req, res) => { app.get('/games/:gameId/npcs', (req, res) => {
const gameId = req.params.gameId; const gameId = req.params.gameId;
db.get('SELECT * FROM NPC WHERE GameID = ?', [gameId], (err, row) => { db.all('SELECT * FROM NPC WHERE GameID = ?', [gameId], (err, row) => {
if (err) { if (err) {
return res.status(500).json({ error: 'Internal server error' }); return res.status(500).json({ error: 'Internal server error' });
} }
@@ -184,12 +206,19 @@ app.get('/games/:gameId/npcs', (req, res) => {
// Fetch game Items // Fetch game Items
app.get('/games/:gameId/items', (req, res) => { app.get('/games/:gameId/items', (req, res) => {
const gameId = req.params.gameId; const gameId = req.params.gameId;
db.get('SELECT * FROM Item WHERE GameID = ?', [gameId], (err, row) => { db.all(`
SELECT Item.*, PlayerCharacter.CharName as OwnerName
FROM Item
LEFT JOIN PlayerCharacter ON Item.OwnerID = PlayerCharacter.CharID
WHERE Item.GameID = ?`,
[gameId],
(err, rows) => {
if (err) { if (err) {
return res.status(500).json({ error: 'Internal server error' }); return res.status(500).json({ error: 'Internal server error' });
} }
res.json(row); res.json(rows);
}); }
);
}) })
@@ -324,6 +353,38 @@ app.get('/games/:gameId/:charId/items', (req, res) => {
}); });
// Item Part // Item Part
// Create Item
app.post('/games/item/create', upload.single('image'), (req, res) => {
const {
ItemName, Type, Art, Rarity, MaxDurability, CurrentDurability,
GoldValue, Abilities, GameID, AP
} = req.body;
const imageBuffer = req.file ? req.file.buffer : null;
const stmt = db.prepare(`
INSERT INTO Item (
ItemName, Type, Art, Rarity, MaxDurability, CurrentDurability,
GoldValue, Abilities, GameID, OwnerID, img, AP
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
`);
stmt.run([
ItemName, Type, Art, Rarity, MaxDurability, CurrentDurability,
GoldValue, Abilities, GameID, imageBuffer, AP
], function(err) {
if (err) {
console.error('Database error:', err);
return res.status(500).json({ error: 'Internal server error' });
}
res.status(201).json({
message: 'Item created successfully!',
itemId: this.lastID
});
});
stmt.finalize();
});
// Set Item Owner // Set Item Owner
app.post('/games/:charId/:itemId/owner', (req, res) => { app.post('/games/:charId/:itemId/owner', (req, res) => {
const gameId = req.params.gameId; const gameId = req.params.gameId;
+11
View File
@@ -10,6 +10,8 @@ import JoinGame from './pages/joinGame';
import StartGame from './pages/startGame'; import StartGame from './pages/startGame';
import Profile from './pages/profile'; import Profile from './pages/profile';
import Games from './pages/games.jsx'; import Games from './pages/games.jsx';
import GameMasterPage from './pages/gameMasterPage.jsx';
import CreateItem from './pages/createItem.jsx';
import CreateCharacter from './pages/createCharacter.jsx'; import CreateCharacter from './pages/createCharacter.jsx';
import { UserProvider } from './context/UserContext.jsx'; import { UserProvider } from './context/UserContext.jsx';
@@ -61,6 +63,10 @@ function App() {
path="/profile" path="/profile"
element={<Profile isLoggedIn={isLoggedIn} />} element={<Profile isLoggedIn={isLoggedIn} />}
/> />
<Route
path='/games/:gameId/master'
element={<GameMasterPage isLoggedIn={isLoggedIn} />}
/>
<Route <Route
path='/games/:gameId' path='/games/:gameId'
element={<Games isLoggedIn={isLoggedIn} />} element={<Games isLoggedIn={isLoggedIn} />}
@@ -69,6 +75,11 @@ function App() {
path='/create-Character' path='/create-Character'
element={<CreateCharacter isLoggedIn={isLoggedIn} />} element={<CreateCharacter isLoggedIn={isLoggedIn} />}
/> />
<Route
path='/create-item'
element={<CreateItem isLoggedIn={isLoggedIn} />}
/>
</Routes> </Routes>
</div> </div>
</Router> </Router>
+308
View File
@@ -0,0 +1,308 @@
import React, { useState, useContext } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { UserContext } from '../context/UserContext';
import axios from 'axios';
import { Box, TextField, Button, Typography, Select, MenuItem, FormControl, InputLabel, Grid2, Card, CardContent, CardMedia } from '@mui/material';
import defaultItemImage from '../assets/default-item.png';
const CreateItem = () => {
const navigate = useNavigate();
const location = useLocation();
const { userId } = useContext(UserContext);
const gameId = new URLSearchParams(location.search).get('gameId');
const [formData, setFormData] = useState({
ItemName: '', // Match DB column names exactly
Type: '',
Art: '',
Rarity: 1,
MaxDurability: 100,
CurrentDurability: 100,
GoldValue: 0,
Abilities: '',
AP: 0,
GameID: gameId
});
const [selectedImage, setSelectedImage] = useState(null);
const [imagePreview, setImagePreview] = useState(null);
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleImageChange = (e) => {
const file = e.target.files[0];
if (file) {
setSelectedImage(file);
setImagePreview(URL.createObjectURL(file));
}
};
const handleSubmit = async (e) => {
e.preventDefault();
const formDataToSend = new FormData();
// Add all form fields with exact column names
Object.keys(formData).forEach(key => {
// Convert null values to empty strings to prevent SQL issues
formDataToSend.append(key, formData[key] === null ? '' : formData[key]);
});
// Add image if present
if (selectedImage) {
formDataToSend.append('image', selectedImage);
}
try {
const response = await axios.post(
'http://localhost:5000/games/item/create',
formDataToSend,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
if (response.status === 201) {
navigate(`/games/${gameId}/master`);
}
} catch (error) {
console.error('Error creating item:', error);
}
};
const inputStyles = {
'& .MuiOutlinedInput-root': {
'& fieldset': { borderColor: '#444' },
'&:hover fieldset': { borderColor: '#764ACB' },
'&.Mui-focused fieldset': { borderColor: '#764ACB' },
},
'& .MuiInputLabel-root': { color: '#fff' },
'& .MuiInputBase-input': { color: '#fff' }
};
return (
<Box sx={{ p: 3, background: 'rgba(30, 30, 47, 0.9)', borderRadius: '8px', marginTop: '40px' }}>
<form onSubmit={handleSubmit}>
<Grid2 container spacing={3}>
{/* Left Column - Image and Basic Info */}
<Grid2 item xs={12} md={4}>
<Box sx={{ border: '1px solid #444', borderRadius: '3px', p: 2, backgroundColor: '#2e2e3f', width: '330px' }}>
<Card sx={{ backgroundColor: '#1e1e2f', color: '#fff' }}>
{/* Image Upload Section */}
{imagePreview ? (
<Box component="label" htmlFor="image-upload" sx={{ cursor: 'pointer', position: 'relative' }}>
<input
accept="image/*"
type="file"
id="image-upload"
style={{ display: 'none' }}
onChange={handleImageChange}
/>
<CardMedia
component="img"
height="300"
image={imagePreview}
alt="Item Preview"
sx={{ borderRadius: '3px', objectFit: 'contain', margin: '0 auto', borderBottom: '1px solid #444' }}
/>
</Box>
) : (
<Box
component="label"
htmlFor="image-upload"
sx={{
height: 300,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#2e2e3f',
borderRadius: '3px',
cursor: 'pointer'
}}
>
<input
accept="image/*"
type="file"
id="image-upload"
style={{ display: 'none' }}
onChange={handleImageChange}
/>
<Button
variant="contained"
component="span"
sx={{
backgroundColor: '#764ACB',
'&:hover': { backgroundColor: '#9865f7' }
}}
>
Upload Item Image
</Button>
</Box>
)}
{/* Basic Item Info */}
<CardContent sx={{ p: 3 }}>
<TextField
fullWidth
label="Item Name"
name="ItemName" // Changed from itemName
value={formData.ItemName}
onChange={handleChange}
required
sx={{ ...inputStyles, mb: 2 }}
/>
<Grid2 container spacing={2}>
<Grid2 item xs={6}>
<FormControl fullWidth sx={{ mb: 2, width: '140px' }}> {/* Width for "Consumable" + padding */}
<InputLabel sx={{ color: '#fff' }}>Type</InputLabel>
<Select
name="Type"
value={formData.Type}
onChange={handleChange}
required
sx={{ ...inputStyles }}
>
<MenuItem value="Weapon">Weapon</MenuItem>
<MenuItem value="Armor">Armor</MenuItem>
<MenuItem value="Consumable">Consumable</MenuItem>
<MenuItem value="Quest">Quest Item</MenuItem>
<MenuItem value="Other">Other</MenuItem>
</Select>
</FormControl>
</Grid2>
<Grid2 item xs={6}>
<FormControl fullWidth sx={{ mb: 2, width: '120px' }}> {/* Width for "Potion" + padding */}
<InputLabel sx={{ color: '#fff' }}>Art</InputLabel>
<Select
name="Art"
value={formData.Art}
onChange={handleChange}
required
sx={{ ...inputStyles }}
>
<MenuItem value="Sword">Sword</MenuItem>
<MenuItem value="Axe">Axe</MenuItem>
<MenuItem value="Bow">Bow</MenuItem>
<MenuItem value="Shield">Shield</MenuItem>
<MenuItem value="Staff">Staff</MenuItem>
<MenuItem value="Potion">Potion</MenuItem>
<MenuItem value="Other">Other</MenuItem>
</Select>
</FormControl>
</Grid2>
</Grid2>
<TextField
fullWidth
label="Gold Value"
name="GoldValue"
type="number"
value={formData.GoldValue}
onChange={handleChange}
required
sx={{ ...inputStyles, mb: 2 }}
/>
</CardContent>
</Card>
</Box>
</Grid2>
{/* Right Column - Details */}
<Grid2 item xs={12} md={8}>
{/* Item Properties */}
<Box sx={{ mb: 4 }}>
<Typography variant="h5" sx={{ color: '#fff', mb: 2 }}>Item Properties</Typography>
<Box sx={{ backgroundColor: '#2e2e3f', p: 2, borderRadius: '8px', border: '1px solid #444', width: '800px', justifyItems: 'center' }}>
<Grid2 container spacing={2}>
<Grid2 item xs={12} md={4}>
<FormControl fullWidth sx={{ mb: 2, width: '160px' }}> {/* Width for "Legendary" + padding */}
<InputLabel sx={{ color: '#fff' }}>Rarity</InputLabel>
<Select
name="Rarity"
value={formData.Rarity}
onChange={handleChange}
required
sx={{ ...inputStyles }}
>
<MenuItem value={1}>Poor</MenuItem>
<MenuItem value={2}>Common</MenuItem>
<MenuItem value={3}>Uncommon</MenuItem>
<MenuItem value={4}>Rare</MenuItem>
<MenuItem value={5}>Epic</MenuItem>
<MenuItem value={6}>Legendary</MenuItem>
<MenuItem value={7}>Artifact</MenuItem>
</Select>
</FormControl>
</Grid2>
<Grid2 item xs={12} md={4}>
<TextField
fullWidth
label="Attack Power"
name="AP"
type="number"
value={formData.AP}
onChange={handleChange}
sx={{ ...inputStyles }}
/>
</Grid2>
<Grid2 item xs={12} md={4}>
<TextField
fullWidth
label="Max Durability"
name="MaxDurability"
type="number"
value={formData.MaxDurability}
onChange={handleChange}
required
sx={{ ...inputStyles }}
/>
</Grid2>
</Grid2>
</Box>
</Box>
{/* Abilities */}
<Box sx={{ mb: 4 }}>
<Typography variant="h5" sx={{ color: '#fff', mb: 2 }}>Abilities</Typography>
<Box sx={{ backgroundColor: '#2e2e3f', p: 2, borderRadius: '8px', border: '1px solid #444' }}>
<TextField
fullWidth
multiline
rows={2}
name="Abilities"
value={formData.Abilities}
onChange={handleChange}
sx={{ ...inputStyles }}
/>
</Box>
</Box>
{/* Submit Button */}
<Button
type="submit"
variant="contained"
fullWidth
sx={{
backgroundColor: '#764ACB',
'&:hover': { backgroundColor: '#9865f7' },
mt: 3
}}
>
Create Item
</Button>
</Grid2>
</Grid2>
</form>
</Box>
);
};
export default CreateItem;
View File
+267
View File
@@ -0,0 +1,267 @@
import React, { useState, useEffect, useContext } from 'react';
import { Link, useParams } from 'react-router-dom';
import { UserContext } from '../context/UserContext';
import axios from 'axios';
import { Box, Typography, Grid2, Card, CardContent, CardMedia, Button, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import defaultCharacterImage from '../assets/default-character.png';
import defaultItemImage from '../assets/default-item.png';
const GameMasterPage = () => {
const { userId } = useContext(UserContext);
const { gameId } = useParams();
const [playerCharacters, setPlayerCharacters] = useState([]);
const [npcs, setNpcs] = useState([]);
const [items, setItems] = useState([]);
// Fix useEffect data fetching
useEffect(() => {
const fetchData = async () => {
try {
// Fetch player characters
const pcsResponse = await axios.get(`http://localhost:5000/games/${gameId}/playerchars`);
const processedPCs = Array.isArray(pcsResponse.data) ? pcsResponse.data : [pcsResponse.data];
setPlayerCharacters(processedPCs.filter(pc => pc !== null));
// Fetch NPCs with different structure
const npcsResponse = await axios.get(`http://localhost:5000/games/${gameId}/npcs`);
const processedNPCs = Array.isArray(npcsResponse.data) ? npcsResponse.data : [npcsResponse.data];
setNpcs(processedNPCs.filter(npc => npc !== null));
// Fetch Items
const itemsResponse = await axios.get(`http://localhost:5000/games/${gameId}/items`);
const processedItems = Array.isArray(itemsResponse.data) ? itemsResponse.data : [itemsResponse.data];
setItems(processedItems.filter(item => item !== null));
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
const interval = setInterval(fetchData, 5000);
return () => clearInterval(interval);
}, [gameId]);
const Section = ({ title, items, createPath, createText }) => (
<Box sx={{
mb: 4,
backgroundColor: '#2e2e3f',
p: 2,
borderRadius: '8px',
border: '1px solid #444'
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h5" sx={{ color: '#fff' }}>{title}</Typography>
<Button
variant="contained"
component={Link}
to={`${createPath}?gameId=${gameId}`}
startIcon={<AddIcon />}
sx={{
backgroundColor: '#764ACB',
'&:hover': { backgroundColor: '#9865f7' }
}}
>
{createText}
</Button>
</Box>
<Grid2 container spacing={2}>
{items.map((item, index) => (
<Grid2 item xs={12} sm={6} md={3} key={index}>
<Card sx={{
backgroundColor: '#1e1e2f',
color: '#fff',
height: '95%', // Reduced height
border: '1px solid #444',
}}>
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '8px'
}}>
<CardMedia
component="img"
height="135" // Reduced from 140
image={item.Img || defaultItemImage}
alt={item.ItemName}
className={`rarity-${item.Rarity} rarity-image`}
sx={{
objectFit: 'contain',
width: '128px'
}}
/>
</Box>
<CardContent>
<Typography
variant="h6"
component="div"
className={`rarity-name-${item.Rarity}`}
>
{item.ItemName}
</Typography>
<Typography variant="body2" sx={{ color: '#bbb' }}>
Type: {item.Type}
</Typography>
<Typography variant="body2" sx={{ color: '#bbb' }}>
Value: {item.GoldValue}
</Typography>
<Typography variant="body2" sx={{ color: '#bbb' }}>
Owner: {item.OwnerName || 'Unassigned'}
</Typography>
</CardContent>
</Card>
</Grid2>
))}
</Grid2>
</Box>
);
return (
// Update main container Box styling
<Box sx={{
p: 3,
background: 'rgba(30, 30, 47, 0.9)',
borderRadius: '8px',
width: '1200px',
margin: '80px auto 40px auto', // Increased top margin
position: 'relative',
minHeight: 'calc(100vh - 120px)' // Account for margins
}}>
<Typography variant="h4" sx={{ mb: 4, color: '#fff' }}>
Game Master Dashboard
</Typography>
{/* Player Characters Section */}
<Box sx={{
mb: 4,
backgroundColor: '#2e2e3f',
p: 2,
borderRadius: '8px',
border: '1px solid #444'
}}>
<Typography variant="h5" sx={{ color: '#fff', mb: 2 }}>Player Characters</Typography>
<Grid2 container spacing={2}>
{playerCharacters.map((character, index) => (
<Grid2 item xs={12} sm={6} md={3} key={index}>
<Card sx={{
backgroundColor: '#1e1e2f',
color: '#fff',
border: '1px solid #444',
height: '100%'
}}>
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '8px'
}}>
<CardMedia
component="img"
height="140"
image={character.Img || defaultCharacterImage}
alt={character.CharName}
sx={{
objectFit: 'contain',
width: '140px',
borderRadius: '3px'
}}
/>
</Box>
<CardContent>
<Typography variant="h6" component="div">
{character.CharName}
</Typography>
<Typography variant="body2" sx={{ color: '#bbb' }}>
{character.Race} - Level {character.Level}
</Typography>
<Typography variant="body2" sx={{ color: '#bbb' }}>
{character.Job}
</Typography>
</CardContent>
</Card>
</Grid2>
))}
</Grid2>
</Box>
{/* NPCs Section */}
<Box sx={{
mb: 4,
backgroundColor: '#2e2e3f',
p: 2,
borderRadius: '8px',
border: '1px solid #444'
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h5" sx={{ color: '#fff' }}>NPCs</Typography>
<Button
variant="contained"
component={Link}
to={`/create-npc?gameId=${gameId}`}
startIcon={<AddIcon />}
sx={{
backgroundColor: '#764ACB',
'&:hover': { backgroundColor: '#9865f7' }
}}
>
Create NPC
</Button>
</Box>
<Grid2 container spacing={2}>
{npcs.map((npc, index) => (
<Grid2 item xs={12} sm={6} md={3} key={index}>
<Card sx={{
backgroundColor: '#1e1e2f',
color: '#fff',
border: '1px solid #444',
height: '100%'
}}>
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '8px'
}}>
<CardMedia
component="img"
height="140"
image={npc.Img || defaultCharacterImage}
alt={npc.CharName}
sx={{
objectFit: 'contain',
width: '140px',
borderRadius: '3px'
}}
/>
</Box>
<CardContent>
<Typography variant="h6" component="div">
{npc.CharName}
</Typography>
<Typography variant="body2" sx={{ color: '#bbb' }}>
{npc.Race}
</Typography>
<Typography variant="body2" sx={{ color: '#bbb' }}>
{npc.Job}
</Typography>
</CardContent>
</Card>
</Grid2>
))}
</Grid2>
</Box>
{/* Items Section */}
<Section
title="Items"
items={items}
createPath="/create-item"
createText="Create Item"
/>
</Box>
);
};
export default GameMasterPage;
+25 -1
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useContext } from 'react'; import { useState, useEffect, useContext } from 'react';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams, useNavigate } from 'react-router-dom';
import axios from 'axios'; import axios from 'axios';
import { UserContext } from '../context/UserContext'; import { UserContext } from '../context/UserContext';
import { Box, Typography, Grid2, Card, CardContent, CardMedia, Button, IconButton, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material'; import { Box, Typography, Grid2, Card, CardContent, CardMedia, Button, IconButton, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material';
@@ -21,6 +21,7 @@ import './games.css';
const GamesPage = () => { const GamesPage = () => {
const { userId } = useContext(UserContext); const { userId } = useContext(UserContext);
const { gameId } = useParams(); const { gameId } = useParams();
const navigate = useNavigate();
const [character, setCharacter] = useState(null); const [character, setCharacter] = useState(null);
const [inventory, setInventory] = useState([]); const [inventory, setInventory] = useState([]);
const [isEditOpen, setIsEditOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false);
@@ -68,6 +69,29 @@ const GamesPage = () => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [character]); }, [character]);
useEffect(() => {
const checkGameMaster = async () => {
if (!userId || !gameId) return;
try {
console.log('Checking if user is game master...');
const response = await axios.get(`http://localhost:5000/games/${gameId}/master`);
console.log('Game data:', response.data);
console.log('User ID:', userId);
console.log('Game master ID:', response.data.game_master_id);
if (parseInt(userId) === parseInt(response.data.game_master_id)) {
console.log('User is game master, redirecting...');
navigate(`/games/${gameId}/master`, { replace: true });
}
} catch (error) {
console.error('Error checking game master:', error);
}
};
checkGameMaster();
}, [userId, gameId, navigate]);
const handleEditOpen = () => { const handleEditOpen = () => {
setNewDescription(character.description); setNewDescription(character.description);
setIsEditOpen(true); setIsEditOpen(true);