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:
2026-01-21 15:02:53 -05:00
parent d382953fdf
commit bd7225c84e
3 changed files with 228 additions and 17 deletions

View File

@@ -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),
@@ -95,10 +96,12 @@ export function DP2Form() {
const selectedCrime = CRIMES.find(c => c.id === form.watch('crimeId'));
const filteredCrimes = CRIMES.filter(crime =>
const filteredCrimes = CRIMES.filter(crime =>
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>

View File

@@ -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,
};
}

View File

@@ -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`);
@@ -385,10 +428,148 @@ export function generateCommands(playerName: string, totalPoints: number, punish
commands.push(`/demote ${playerName} standard`);
break;
}
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('_', ' ');