feat: enhance DP2 moderation assistant with detailed command reasons and severity levels
- Replace generic 'DP2 violation' with specific crime-based reasons - Add severity levels to reasons (Minor/Moderate/Severe Theft, Minor/Moderate/Large/Massive Griefing) - Fix ban command generation to use tempban/tempmute with proper time formatting instead of ban - Fix theft-grief point calculation to use blockCount / 20 with minimum 2 points - Fix dark/light mode toggle button functionality - Clear results when category or crime selection changes - Improve command accuracy using AdvancedBan syntax (/tempban, /tempmute, etc.)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
@@ -42,7 +42,8 @@ type FormData = z.infer<typeof FormSchema>;
|
||||
|
||||
export function DP2Form() {
|
||||
const [activeCategory, setActiveCategory] = useState<Crime['category'] | null>(null);
|
||||
const { result, isLoading, calculatePunishment, copyToClipboard } = useDP2Calculator();
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const { result, isLoading, calculatePunishment, copyToClipboard, clearResult } = useDP2Calculator();
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
@@ -99,6 +100,8 @@ export function DP2Form() {
|
||||
activeCategory ? crime.category === activeCategory : true
|
||||
);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<Card>
|
||||
@@ -107,7 +110,15 @@ export function DP2Form() {
|
||||
<CardTitle>dp2 moderation assistant</CardTitle>
|
||||
<CardDescription>calculate punishments and generate commands based on dp2 guidelines</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="ml-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto"
|
||||
onClick={() => {
|
||||
setIsDarkMode(!isDarkMode);
|
||||
document.documentElement.classList.toggle('dark');
|
||||
}}
|
||||
>
|
||||
<Moon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Sun className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
@@ -156,6 +167,7 @@ export function DP2Form() {
|
||||
onClick={() => {
|
||||
setActiveCategory(null);
|
||||
form.setValue('crimeId', '');
|
||||
clearResult();
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
@@ -167,6 +179,7 @@ export function DP2Form() {
|
||||
onClick={() => {
|
||||
setActiveCategory("item");
|
||||
form.setValue('crimeId', '');
|
||||
clearResult();
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
@@ -178,6 +191,7 @@ export function DP2Form() {
|
||||
onClick={() => {
|
||||
setActiveCategory("block");
|
||||
form.setValue('crimeId', '');
|
||||
clearResult();
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
@@ -189,6 +203,7 @@ export function DP2Form() {
|
||||
onClick={() => {
|
||||
setActiveCategory("hacking");
|
||||
form.setValue('crimeId', '');
|
||||
clearResult();
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
@@ -200,6 +215,7 @@ export function DP2Form() {
|
||||
onClick={() => {
|
||||
setActiveCategory("communication");
|
||||
form.setValue('crimeId', '');
|
||||
clearResult();
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
@@ -212,7 +228,10 @@ export function DP2Form() {
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="crimeId">specific offense</Label>
|
||||
<Select
|
||||
onValueChange={(value) => form.setValue('crimeId', value)}
|
||||
onValueChange={(value) => {
|
||||
form.setValue('crimeId', value);
|
||||
clearResult();
|
||||
}}
|
||||
value={form.watch('crimeId')}
|
||||
>
|
||||
<SelectTrigger>
|
||||
|
||||
@@ -99,6 +99,12 @@ export function useDP2Calculator() {
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for theft-grief classification
|
||||
if (crime.id === 'theft_grief') {
|
||||
const blockCount = offenseData.blockCount || 0;
|
||||
basePoints = Math.max(2, Math.floor(blockCount / 20));
|
||||
}
|
||||
|
||||
// Add any additional points from items (for non-theft crimes)
|
||||
if (crime.id !== 'theft') {
|
||||
basePoints += itemPoints + specialItemPoints;
|
||||
@@ -111,7 +117,7 @@ export function useDP2Calculator() {
|
||||
const punishmentLevel = getPunishmentLevel(totalPoints, crime.category, offenseData.isSPP);
|
||||
|
||||
// Generate commands
|
||||
const commands = generateCommands(playerData.playerName, totalPoints, punishmentLevel);
|
||||
const commands = generateCommands(playerData.playerName, totalPoints, punishmentLevel, crime, offenseData);
|
||||
|
||||
// Generate explanation
|
||||
const explanation = generateExplanation(crime, basePoints, totalPoints, punishmentLevel);
|
||||
@@ -144,10 +150,15 @@ export function useDP2Calculator() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearResult = useCallback(() => {
|
||||
setResult(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
result,
|
||||
isLoading,
|
||||
calculatePunishment,
|
||||
copyToClipboard,
|
||||
clearResult,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export const CRIMES: Crime[] = [
|
||||
id: 'theft_grief',
|
||||
name: 'Theft-Grief',
|
||||
category: 'block',
|
||||
description: '< 100 blocks destroyed, primarily valuables',
|
||||
description: 'Blocks destroyed, primarily valuables - points = max(2, blockCount / 20)',
|
||||
basePoints: 2,
|
||||
requiresBlockDetails: true,
|
||||
},
|
||||
@@ -337,43 +337,86 @@ export function getPunishmentLevel(points: number, crimeCategory: CrimeCategory,
|
||||
return '4week_ban';
|
||||
}
|
||||
|
||||
export function generateCommands(playerName: string, totalPoints: number, punishmentLevel: PunishmentLevel): string[] {
|
||||
export function generateCommands(
|
||||
playerName: string,
|
||||
totalPoints: number,
|
||||
punishmentLevel: PunishmentLevel,
|
||||
crime: Crime,
|
||||
offenseData?: {
|
||||
itemDetails?: Array<{ type: string; quantity: number }>;
|
||||
blockCount?: number;
|
||||
specialItems?: {
|
||||
elytra: number;
|
||||
netherStar: number;
|
||||
beacon: number;
|
||||
netheriteBlock: number;
|
||||
diamondBlock: number;
|
||||
};
|
||||
}
|
||||
): string[] {
|
||||
const commands: string[] = [];
|
||||
|
||||
// Always add note command
|
||||
commands.push(`/note ${playerName} ${totalPoints}`);
|
||||
|
||||
// Generate specific reason based on crime with severity details
|
||||
const reason = getCrimeReason(crime, offenseData);
|
||||
|
||||
// Add punishment command based on level
|
||||
switch (punishmentLevel) {
|
||||
case 'warning':
|
||||
commands.push(`/warn ${playerName} DP2 violation`);
|
||||
commands.push(`/warn ${playerName} ${reason}`);
|
||||
break;
|
||||
case '15min_mute':
|
||||
commands.push(`/tempmute ${playerName} 15m ${reason}`);
|
||||
break;
|
||||
case '30min_mute':
|
||||
commands.push(`/tempmute ${playerName} 30m ${reason}`);
|
||||
break;
|
||||
case '1hour_mute':
|
||||
commands.push(`/tempmute ${playerName} 1h ${reason}`);
|
||||
break;
|
||||
case '1day_mute':
|
||||
commands.push(`/mute ${playerName} ${punishmentLevel.replace('_', ' ')}`);
|
||||
commands.push(`/tempmute ${playerName} 1d ${reason}`);
|
||||
break;
|
||||
case '12hour_vc_ban':
|
||||
commands.push(`/vcban ${playerName} 12h ${reason}`);
|
||||
break;
|
||||
case '1day_vc_ban':
|
||||
commands.push(`/vcban ${playerName} 1d ${reason}`);
|
||||
break;
|
||||
case 'permanent_vc_ban':
|
||||
commands.push(`/vcban ${playerName} ${punishmentLevel.replace('_', ' ')}`);
|
||||
commands.push(`/vcban ${playerName} permanent ${reason}`);
|
||||
break;
|
||||
case 'kick':
|
||||
commands.push(`/kick ${playerName} DP2 violation`);
|
||||
commands.push(`/kick ${playerName} ${reason}`);
|
||||
break;
|
||||
case '1day_ban':
|
||||
commands.push(`/tempban ${playerName} 1d ${reason}`);
|
||||
break;
|
||||
case '2day_ban':
|
||||
commands.push(`/tempban ${playerName} 2d ${reason}`);
|
||||
break;
|
||||
case '3day_ban':
|
||||
commands.push(`/tempban ${playerName} 3d ${reason}`);
|
||||
break;
|
||||
case '5day_ban':
|
||||
commands.push(`/tempban ${playerName} 5d ${reason}`);
|
||||
break;
|
||||
case '1week_ban':
|
||||
commands.push(`/tempban ${playerName} 1w ${reason}`);
|
||||
break;
|
||||
case '2week_ban':
|
||||
commands.push(`/tempban ${playerName} 2w ${reason}`);
|
||||
break;
|
||||
case '4week_ban':
|
||||
commands.push(`/tempban ${playerName} 4w ${reason}`);
|
||||
break;
|
||||
case '1month_ban':
|
||||
commands.push(`/ban ${playerName} ${punishmentLevel.replace('_ban', '')}`);
|
||||
commands.push(`/tempban ${playerName} 1mo ${reason}`);
|
||||
break;
|
||||
case 'permanent_ban':
|
||||
commands.push(`/ban ${playerName} permanent`);
|
||||
commands.push(`/ban ${playerName} ${reason}`);
|
||||
break;
|
||||
case 'wipe_inventory':
|
||||
commands.push(`/wipe ${playerName} inventory`);
|
||||
@@ -389,6 +432,144 @@ export function generateCommands(playerName: string, totalPoints: number, punish
|
||||
return commands;
|
||||
}
|
||||
|
||||
function getCrimeReason(
|
||||
crime: Crime,
|
||||
offenseData?: {
|
||||
itemDetails?: Array<{ type: string; quantity: number }>;
|
||||
blockCount?: number;
|
||||
specialItems?: {
|
||||
elytra: number;
|
||||
netherStar: number;
|
||||
beacon: number;
|
||||
netheriteBlock: number;
|
||||
diamondBlock: number;
|
||||
};
|
||||
}
|
||||
): string {
|
||||
// Calculate severity for theft and grief
|
||||
let severity = '';
|
||||
|
||||
if (crime.id === 'theft' && offenseData) {
|
||||
const totalItemPoints = calculateItemPoints(offenseData);
|
||||
if (totalItemPoints < 50) {
|
||||
severity = 'Minor ';
|
||||
} else if (totalItemPoints <= 500) {
|
||||
severity = 'Moderate ';
|
||||
} else {
|
||||
severity = 'Severe ';
|
||||
}
|
||||
}
|
||||
|
||||
if (crime.id === 'grief' && offenseData?.blockCount !== undefined) {
|
||||
const blockCount = offenseData.blockCount;
|
||||
if (blockCount < 100) {
|
||||
severity = 'Minor ';
|
||||
} else if (blockCount <= 1000) {
|
||||
severity = 'Moderate ';
|
||||
} else if (blockCount <= 100000) {
|
||||
severity = 'Large ';
|
||||
} else {
|
||||
severity = 'Massive ';
|
||||
}
|
||||
}
|
||||
|
||||
// Map crime IDs to specific reasons based on DP2 guidelines
|
||||
switch (crime.id) {
|
||||
// Item Offenses
|
||||
case 'inappropriate_item_names':
|
||||
return 'Inappropriate item names';
|
||||
case 'inappropriate_book_contents':
|
||||
return 'Inappropriate book contents';
|
||||
case 'theft':
|
||||
return `${severity}Theft`;
|
||||
case 'unconsensual_killing':
|
||||
return 'Unconsensual killing';
|
||||
case 'illegal_item_use':
|
||||
return 'Using illegal item';
|
||||
|
||||
// Block Offenses
|
||||
case 'vandalism':
|
||||
return 'Vandalism';
|
||||
case 'grief':
|
||||
return `${severity}Griefing`;
|
||||
case 'theft_grief':
|
||||
return 'Theft-grief';
|
||||
case 'vandalism_infrastructure':
|
||||
return 'Vandalism of public infrastructure';
|
||||
case 'trespassing':
|
||||
return 'Trespassing';
|
||||
case 'trespassing_staff':
|
||||
return 'Trespassing on staff/SPP land';
|
||||
|
||||
// Hacking Offenses
|
||||
case 'x_raying':
|
||||
return 'X-raying';
|
||||
case 'hacking_client':
|
||||
return 'Use of hacking client';
|
||||
case 'lagging_server':
|
||||
return 'Deliberately lagging server';
|
||||
case 'worldedit_misuse':
|
||||
return 'Misuse of WorldEdit';
|
||||
case 'exploit_abuse':
|
||||
return 'Abuse of exploits';
|
||||
|
||||
// Communication Offenses
|
||||
case 'abusive_chat':
|
||||
return 'Abusive chat';
|
||||
case 'inciting_verbal_conflict':
|
||||
return 'Inciting verbal conflict';
|
||||
case 'abusive_vc':
|
||||
return 'Abusive voice chat language';
|
||||
case 'lying_to_staff':
|
||||
return 'Lying to staff member';
|
||||
case 'manipulation':
|
||||
return 'Manipulation';
|
||||
case 'grand_manipulation':
|
||||
return 'Grand manipulation';
|
||||
case 'slander':
|
||||
return 'Slander against SPP';
|
||||
case 'violation_nca':
|
||||
return 'Violation of non-communication agreement';
|
||||
|
||||
default:
|
||||
return 'DP2 violation'; // Fallback for any unmapped crimes
|
||||
}
|
||||
}
|
||||
|
||||
function calculateItemPoints(offenseData: {
|
||||
itemDetails?: Array<{ type: string; quantity: number }>;
|
||||
specialItems?: {
|
||||
elytra: number;
|
||||
netherStar: number;
|
||||
beacon: number;
|
||||
netheriteBlock: number;
|
||||
diamondBlock: number;
|
||||
};
|
||||
}): number {
|
||||
let itemPoints = 0;
|
||||
|
||||
// Calculate special item points
|
||||
if (offenseData.specialItems) {
|
||||
itemPoints +=
|
||||
(offenseData.specialItems.elytra * (ITEM_POINTS['elytra'] || 20)) +
|
||||
(offenseData.specialItems.netherStar * (ITEM_POINTS['nether_star'] || 20)) +
|
||||
(offenseData.specialItems.beacon * (ITEM_POINTS['beacon'] || 20)) +
|
||||
(offenseData.specialItems.netheriteBlock * (ITEM_POINTS['netherite_block'] || 10)) +
|
||||
(offenseData.specialItems.diamondBlock * (ITEM_POINTS['diamond_block'] || 10));
|
||||
}
|
||||
|
||||
// Calculate regular item points
|
||||
if (offenseData.itemDetails) {
|
||||
for (const item of offenseData.itemDetails) {
|
||||
const itemKey = item.type.toLowerCase() as keyof typeof ITEM_POINTS;
|
||||
const pointValue = ITEM_POINTS[itemKey] || ITEM_POINTS['other'] || 1;
|
||||
itemPoints += pointValue * (item.quantity || 0);
|
||||
}
|
||||
}
|
||||
|
||||
return itemPoints;
|
||||
}
|
||||
|
||||
export function generateExplanation(crime: Crime, basePoints: number, totalPoints: number, punishmentLevel: PunishmentLevel): string {
|
||||
const punishmentText = punishmentLevel.replace('_', ' ');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user