feat: add minecraftStats to player model and update related API endpoints

This commit is contained in:
Lars Behrends
2025-12-29 09:27:06 +01:00
parent abebf8f7a2
commit 5fe6724663
4 changed files with 80 additions and 19 deletions

View File

@@ -23,6 +23,7 @@ const SEED_PLAYERS = [
isAdmin: 0, isAdmin: 0,
tags: JSON.stringify(['#Bürger', '#Händler']), tags: JSON.stringify(['#Bürger', '#Händler']),
stats: JSON.stringify({ playtimeHours: 482, level: 45, role: 'Bürger', organizationId: 'org-3' }), stats: JSON.stringify({ playtimeHours: 482, level: 45, role: 'Bürger', organizationId: 'org-3' }),
minecraftStats: null,
inventory: JSON.stringify([]), inventory: JSON.stringify([]),
storyMarkdown: '# Der Bauplan von V\n\n> "Stein erinnert sich..."' storyMarkdown: '# Der Bauplan von V\n\n> "Stein erinnert sich..."'
}, },
@@ -34,6 +35,7 @@ const SEED_PLAYERS = [
isAdmin: 1, // DrKButz is admin for testing isAdmin: 1, // DrKButz is admin for testing
tags: JSON.stringify(['#Bauunternehmer']), tags: JSON.stringify(['#Bauunternehmer']),
stats: JSON.stringify({ playtimeHours: 120, level: 12, role: 'Unternehmer', organizationId: 'org-4' }), stats: JSON.stringify({ playtimeHours: 120, level: 12, role: 'Unternehmer', organizationId: 'org-4' }),
minecraftStats: null,
inventory: JSON.stringify([]), inventory: JSON.stringify([]),
storyMarkdown: '# Forschungslogbuch:\n\nSpezialisiert auf...' storyMarkdown: '# Forschungslogbuch:\n\nSpezialisiert auf...'
} }
@@ -117,6 +119,7 @@ function setupTables() {
isAdmin TINYINT DEFAULT 0, isAdmin TINYINT DEFAULT 0,
tags JSON, tags JSON,
stats JSON, stats JSON,
minecraftStats JSON,
inventory JSON, inventory JSON,
storyMarkdown TEXT, storyMarkdown TEXT,
discordId VARCHAR(255) discordId VARCHAR(255)

View File

@@ -152,9 +152,11 @@ app.get('/api/players', (req, res) => {
// Parse JSON fields // Parse JSON fields
const parsed = rows.map(r => ({ const parsed = rows.map(r => ({
...r, ...r,
tags: JSON.parse(r.tags), tags: JSON.parse(r.tags || '[]'),
stats: JSON.parse(r.stats), stats: JSON.parse(r.minecraftStats || '{}'),
inventory: JSON.parse(r.inventory), //stats: JSON.parse(r.stats || '{}'),
//minecraftStats: r.minecraftStats ? JSON.parse(r.minecraftStats) : undefined,
inventory: JSON.parse(r.inventory || '[]'),
isOnline: !!r.isOnline isOnline: !!r.isOnline
})); }));
res.json(parsed); res.json(parsed);
@@ -474,15 +476,16 @@ app.post('/api/admin/npc-citizen', (req, res) => {
role: role || 'Bürger', role: role || 'Bürger',
organizationId: organizationId || null organizationId: organizationId || null
}), }),
minecraftStats: null,
inventory: JSON.stringify([]), inventory: JSON.stringify([]),
storyMarkdown: storyMarkdown || `# ${username}\n\nEin NPC-Bürger im Obsidian-Tal.`, storyMarkdown: storyMarkdown || `# ${username}\n\nEin NPC-Bürger im Obsidian-Tal.`,
discordId: null discordId: null
}; };
db.run(`INSERT INTO players (uuid, username, isOnline, isNpc, tags, stats, inventory, storyMarkdown, discordId) db.run(`INSERT INTO players (uuid, username, isOnline, isNpc, tags, stats, minecraftStats, inventory, storyMarkdown, discordId)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[npcData.uuid, npcData.username, npcData.isOnline, npcData.isNpc, npcData.tags, [npcData.uuid, npcData.username, npcData.isOnline, npcData.isNpc, npcData.tags,
npcData.stats, npcData.inventory, npcData.storyMarkdown, npcData.discordId], npcData.stats, npcData.minecraftStats, npcData.inventory, npcData.storyMarkdown, npcData.discordId],
function(err) { function(err) {
if (err) { if (err) {
console.error('Error creating NPC citizen:', err); console.error('Error creating NPC citizen:', err);
@@ -1600,7 +1603,8 @@ app.post('/api/data', (req, res) => { const { eventType, player, uuid, timestamp
isNpc: 0, isNpc: 0,
isAdmin: 0, isAdmin: 0,
tags: JSON.stringify([]), tags: JSON.stringify([]),
stats: JSON.stringify({ stats: JSON.stringify({}),
minecraftStats: JSON.stringify({
char, char,
statistics, statistics,
advancements, advancements,
@@ -1620,8 +1624,8 @@ app.post('/api/data', (req, res) => { const { eventType, player, uuid, timestamp
if (row) { if (row) {
// Update existing player // Update existing player
db.run(`UPDATE players SET username = ?, isOnline = ?, stats = ?, storyMarkdown = ? WHERE uuid = ?`, db.run(`UPDATE players SET username = ?, isOnline = ?, minecraftStats = ? WHERE uuid = ?`,
[playerData.username, playerData.isOnline, playerData.stats, playerData.storyMarkdown, uuid], [playerData.username, playerData.isOnline, playerData.minecraftStats, uuid],
function(err) { function(err) {
if (err) { if (err) {
console.error('Error updating player:', err); console.error('Error updating player:', err);
@@ -1637,10 +1641,10 @@ app.post('/api/data', (req, res) => { const { eventType, player, uuid, timestamp
); );
} else { } else {
// Insert new player // Insert new player
db.run(`INSERT INTO players (uuid, username, isOnline, isNpc, isAdmin, tags, stats, inventory, storyMarkdown, discordId) db.run(`INSERT INTO players (uuid, username, isOnline, isNpc, isAdmin, tags, stats, minecraftStats, inventory, storyMarkdown, discordId)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[playerData.uuid, playerData.username, playerData.isOnline, playerData.isNpc, playerData.isAdmin, [playerData.uuid, playerData.username, playerData.isOnline, playerData.isNpc, playerData.isAdmin,
playerData.tags, playerData.stats, playerData.inventory, playerData.storyMarkdown, playerData.discordId], playerData.tags, playerData.stats, playerData.minecraftStats, playerData.inventory, playerData.storyMarkdown, playerData.discordId],
function(err) { function(err) {
if (err) { if (err) {
console.error('Error inserting player:', err); console.error('Error inserting player:', err);

View File

@@ -22,7 +22,7 @@ const PlayerProfile: React.FC = () => {
// Is this the logged-in user's profile? // Is this the logged-in user's profile?
const isOwner = currentUser?.linkedPlayerUuid === player?.uuid; const isOwner = currentUser?.linkedPlayerUuid === player?.uuid;
const playerOrg = player ? dbService.getOrg(player.stats.organizationId || '') : null; const playerOrg = player ? dbService.getOrg(player.stats?.organizationId || '') : null;
// Check if player is already linked to anyone in our mock/real DB logic // Check if player is already linked to anyone in our mock/real DB logic
// Since the Player object doesn't expose 'discordId' publicly in types yet (it's hidden in DB), // Since the Player object doesn't expose 'discordId' publicly in types yet (it's hidden in DB),
@@ -182,7 +182,7 @@ const PlayerProfile: React.FC = () => {
</div> </div>
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
{player.tags.map(tag => ( {(player.tags || []).map(tag => (
<span key={tag} className="text-xs px-2 py-1 bg-surfaceHighlight rounded text-textMuted border border-white/5 font-mono"> <span key={tag} className="text-xs px-2 py-1 bg-surfaceHighlight rounded text-textMuted border border-white/5 font-mono">
{tag} {tag}
</span> </span>
@@ -200,12 +200,41 @@ const PlayerProfile: React.FC = () => {
<div className="flex gap-6 text-sm"> <div className="flex gap-6 text-sm">
<div className="flex items-center gap-2 text-textMuted"> <div className="flex items-center gap-2 text-textMuted">
<Icons.Terminal className="w-4 h-4" /> <Icons.Terminal className="w-4 h-4" />
<span className="font-mono text-textMain">{player.stats.playtimeHours}h</span> <span className="font-mono text-textMain">
{player.minecraftStats?.statistics?.general?.["minecraft:play_time"]
? `${Math.round((player.minecraftStats.statistics.general["minecraft:play_time"] || 0) / 20 / 3600)}h`
: `${player.stats?.playtimeHours || 0}h`
}
</span>
</div> </div>
<div className="flex items-center gap-2 text-textMuted"> <div className="flex items-center gap-2 text-textMuted">
<Icons.Box className="w-4 h-4" /> <Icons.Box className="w-4 h-4" />
<span className="font-mono text-textMain">Lvl {player.stats.level}</span> <span className="font-mono text-textMain">
Lvl {player.minecraftStats?.char?.xpLevel || player.stats?.level || 1}
</span>
</div> </div>
{player.minecraftStats && player.minecraftStats.char && player.minecraftStats.statistics && (
<>
<div className="flex items-center gap-2 text-textMuted">
<Icons.Shield className="w-4 h-4" />
<span className="font-mono text-textMain">
{player.minecraftStats.char.health}/{player.minecraftStats.char.maxHealth} HP
</span>
</div>
<div className="flex items-center gap-2 text-textMuted">
<Icons.Coins className="w-4 h-4" />
<span className="font-mono text-textMain">
{player.minecraftStats.char.foodLevel}/20 Food
</span>
</div>
<div className="flex items-center gap-2 text-textMuted">
<Icons.Hammer className="w-4 h-4" />
<span className="font-mono text-textMain">
{player.minecraftStats.statistics.general?.["minecraft:mob_kills"] || 0} Kills
</span>
</div>
</>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -214,7 +243,7 @@ const PlayerProfile: React.FC = () => {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Col: Inventory & Org */} {/* Left Col: Inventory & Org */}
<div className="lg:col-span-1 space-y-6"> <div className="lg:col-span-1 space-y-6">
<InventoryGrid items={player.inventory} /> <InventoryGrid items={player.inventory || []} />
<div className="bg-surface border border-border rounded-xl p-4 shadow-card"> <div className="bg-surface border border-border rounded-xl p-4 shadow-card">
<h3 className="text-xs font-bold uppercase tracking-wider text-textMuted mb-3">Zugehörigkeit</h3> <h3 className="text-xs font-bold uppercase tracking-wider text-textMuted mb-3">Zugehörigkeit</h3>
@@ -229,7 +258,7 @@ const PlayerProfile: React.FC = () => {
{playerOrg ? playerOrg.name.charAt(0) : <Icons.Map className="w-5 h-5 opacity-50" />} {playerOrg ? playerOrg.name.charAt(0) : <Icons.Map className="w-5 h-5 opacity-50" />}
</div> </div>
<div> <div>
<div className="text-sm font-medium text-textMain">{player.stats.role}</div> <div className="text-sm font-medium text-textMain">{player.stats?.role || 'Unbekannt'}</div>
<div className="text-xs text-textMuted"> <div className="text-xs text-textMuted">
{playerOrg ? ( {playerOrg ? (
<span className="group-hover:text-accentInfo transition-colors">{playerOrg.name}</span> <span className="group-hover:text-accentInfo transition-colors">{playerOrg.name}</span>
@@ -263,7 +292,7 @@ const PlayerProfile: React.FC = () => {
</div> </div>
</div> </div>
<div className="prose-custom text-sm"> <div className="prose-custom text-sm">
{renderMarkdown(player.storyMarkdown)} {renderMarkdown(player.storyMarkdown || '')}
</div> </div>
</div> </div>

View File

@@ -7,6 +7,30 @@ export interface Item {
nbtSummary?: string; nbtSummary?: string;
} }
export interface MinecraftStats {
char: {
health: number;
maxHealth: number;
foodLevel: number;
xpLevel: number;
position: {
x: number;
y: number;
z: number;
};
};
statistics: {
general: { [key: string]: number };
kills: { [key: string]: number };
killed_by: { [key: string]: number };
};
advancements: Array<{
id: string;
title: string;
}>;
lastSync: number;
}
export interface PlayerStats { export interface PlayerStats {
playtimeHours: number; playtimeHours: number;
level: number; level: number;
@@ -20,6 +44,7 @@ export interface Player {
skinUrl?: string; // Placeholder in a real app skinUrl?: string; // Placeholder in a real app
inventory: (Item | null)[]; inventory: (Item | null)[];
stats: PlayerStats; stats: PlayerStats;
minecraftStats?: MinecraftStats;
storyMarkdown: string; storyMarkdown: string;
tags: string[]; tags: string[];
isOnline: boolean; isOnline: boolean;