Items Beheer
Admin interface voor item definities en container types.
Item Definities
Overzicht Pagina
┌─────────────────────────────────────────────────────────┐
│ Items Beheer [+ Nieuw Item]│
├─────────────────────────────────────────────────────────┤
│ Filter: [Alle categorieën ▼] [Zoeken... ] 🔍│
├─────────────────────────────────────────────────────────┤
│ ┌─────┐ honey_jar │ food │ 99 │ common │ ✏️│
│ │ 🍯 │ Honingpot │ │ │ │ │
│ └─────┘ │ │ │ │ │
├─────────────────────────────────────────────────────────┤
│ ┌─────┐ oak_seed │ seed │ 99 │ common │ ✏️│
│ │ 🌰 │ Eikenzaadje │ │ │ │ │
│ └─────┘ │ │ │ │ │
└─────────────────────────────────────────────────────────┘
Item Formulier
// components/ItemForm.tsx
interface ItemFormProps {
item?: ItemDefinition;
onSave: (item: ItemDefinition) => void;
}
export function ItemForm({ item, onSave }: ItemFormProps) {
const [formData, setFormData] = useState({
itemId: item?.itemId ?? '',
category: item?.category ?? 'item',
displayName: item?.displayName ?? '',
description: item?.description ?? '',
iconPath: item?.iconPath ?? '',
maxStack: item?.maxStack ?? 99,
isTradeable: item?.isTradeable ?? true,
rarity: item?.rarity ?? 0,
});
return (
<form onSubmit={handleSubmit}>
{/* Item ID (alleen bij nieuw) */}
{!item && (
<Input
label="Item ID"
value={formData.itemId}
onChange={e => setFormData({...formData, itemId: e.target.value})}
placeholder="honey_jar"
pattern="[a-z_]+"
/>
)}
{/* Categorie */}
<Select
label="Categorie"
value={formData.category}
onChange={e => setFormData({...formData, category: e.target.value})}
>
<option value="food">Voedsel</option>
<option value="seed">Zaden</option>
<option value="tool">Gereedschap</option>
<option value="material">Materiaal</option>
<option value="decoration">Decoratie</option>
</Select>
{/* Display Name */}
<Input
label="Weergavenaam"
value={formData.displayName}
onChange={e => setFormData({...formData, displayName: e.target.value})}
/>
{/* etc... */}
</form>
);
}
Categorieën
| Categorie | Beschrijving | Voorbeelden |
|---|---|---|
| food | Eetbare items | Honing, Appels, Brood |
| seed | Plantbare zaden | Wortelzaad, Tomatenzaad |
| tool | Gereedschap | Schep, Gieter, Bijl |
| material | Grondstoffen | Hout, Steen, IJzer |
| decoration | Decoraties | Bloemen, Beelden |
| container | Rugzakken | Starter bag, Backpack |
| special | Speciale items | Quest items, Keys |
Rarity Levels
| Level | Naam | Kleur | Drop Rate |
|---|---|---|---|
| 0 | Common | Wit | 70% |
| 1 | Uncommon | Groen | 20% |
| 2 | Rare | Blauw | 8% |
| 3 | Legendary | Goud | 2% |
Container Types
Beheer Interface
┌─────────────────────────────────────────────────────────┐
│ Container Types [+ Nieuw Type] │
├─────────────────────────────────────────────────────────┤
│ Type ID │ Naam │ Slots │ Acties │
├─────────────────────────────────────────────────────────┤
│ starter_bag │ Starter Rugzak │ 8 │ ✏️ 🗑️ │
│ leather_backpack │ Leren Rugzak │ 12 │ ✏️ 🗑️ │
│ adventure_pack │ Avonturen Rugzak │ 16 │ ✏️ 🗑️ │
│ pouch_small │ Kleine Buidel │ 4 │ ✏️ 🗑️ │
└─────────────────────────────────────────────────────────┘
Container Balancing
| Tier | Slots | Verkrijgbaar via |
|---|---|---|
| 1 | 8 | Start van spel |
| 2 | 12 | Shop (500 eikels) |
| 3 | 16 | Quest reward |
| 4 | 24 | Club project |
| 5 | 32 | Legendary drop |
Bulk Import/Export
Export naar JSON
async function exportItems() {
const items = await spacetime.query('SELECT * FROM item_definition');
const json = JSON.stringify(items, null, 2);
// Download als bestand
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
downloadFile(url, 'items_export.json');
}
Import JSON
async function importItems(file: File) {
const json = await file.text();
const items = JSON.parse(json);
for (const item of items) {
await spacetime.call('create_item_definition', [
item.item_id,
item.category,
item.display_name,
item.description,
item.icon_path,
item.max_stack,
item.is_tradeable,
item.rarity
]);
}
}
JSON Format
[
{
"item_id": "honey_jar",
"category": "food",
"display_name": "Honingpot",
"description": "Zoete honing van de bijen",
"icon_path": "items/honey_jar.png",
"max_stack": 99,
"is_tradeable": true,
"rarity": 0
},
{
"item_id": "golden_shovel",
"category": "tool",
"display_name": "Gouden Schep",
"description": "Een glimmende schep die sneller graaft",
"icon_path": "items/golden_shovel.png",
"max_stack": 1,
"is_tradeable": false,
"rarity": 3
}
]
Icon Preview
// components/IconPreview.tsx
export function IconPreview({ iconPath }: { iconPath: string }) {
const [error, setError] = useState(false);
// Probeer van R2 storage te laden
const fullUrl = `${process.env.R2_PUBLIC_URL}/assets/${iconPath}`;
if (error) {
return <PlaceholderIcon />;
}
return (
<img
src={fullUrl}
alt="Item icon"
onError={() => setError(true)}
className="w-16 h-16 object-contain"
/>
);
}
Validatie
Item ID
- Alleen lowercase letters en underscores
- 3-50 karakters
- Uniek
Icon Path
- Moet eindigen op
.pngof.svg - Pad relatief aan assets folder
const validateIconPath = (path: string) => {
if (!path.match(/\.(png|svg)$/)) {
return 'Icon moet .png of .svg zijn';
}
if (path.startsWith('/') || path.includes('..')) {
return 'Ongeldig pad';
}
return null;
};