mirror of
https://github.com/ceratic/project_vollidioten_website.git
synced 2026-05-14 00:16:47 +02:00
feat: add minecraftStats to player model and update related API endpoints
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
25
types.ts
25
types.ts
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user