Asset Pipeline
Beheer van art assets via ticket systeem.
Workflow Overzicht
┌─────────┐ ┌─────────────┐ ┌────────┐ ┌──────────┐
│ Open │───▶│ In Progress │───▶│ Review │───▶│ Approved │
└─────────┘ └─────────────┘ └────────┘ └──────────┘
│ │ │
│ │ ▼
│ │ ┌──────────┐
│ └──────────▶│ Revision │
│ └──────────┘
│ │
▼ │
┌─────────┐ │
│Rejected │◀─────────────────────────┘
└─────────┘
Ticket Board
┌─────────────────────────────────────────────────────────────────┐
│ Asset Tickets [+ Nieuw Ticket]│
├─────────────────────────────────────────────────────────────────┤
│ │
│ Open (5) In Progress (3) Review (2) Done (47) │
│ ────────────── ─────────────── ───────── ───────── │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │CHAR-015 │ │CHAR-012 │ │ITEM-089 │ │
│ │NPC Bakker │ │Girl V3 │ │Gouden │ │
│ │🔴 Urgent │ │@designer1 │ │Schep │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ITEM-090 │ │MAP-003 │ │UI-022 │ │
│ │Wortelzaad │ │Bos achter │ │Inventory │ │
│ │🟡 Normal │ │@designer2 │ │slot │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Ticket Aanmaken
interface NewTicket {
categoryId: string;
title: string;
description: string;
referenceUrls: string[];
styleNotes: string;
priority: number;
gamePath: string;
specTemplateId?: number;
}
async function createTicket(ticket: NewTicket) {
const ticketCode = await generateTicketCode(ticket.categoryId);
await spacetime.call('create_asset_ticket', [
ticket.categoryId,
ticket.title,
ticket.description,
JSON.stringify(ticket.referenceUrls),
ticket.styleNotes,
ticket.priority,
ticket.gamePath,
]);
}
// Ticket code generator: CHAR-001, ITEM-042, etc.
async function generateTicketCode(category: string): Promise<string> {
const prefix = category.toUpperCase().substring(0, 4);
const count = await spacetime.query(
`SELECT COUNT(*) FROM asset_ticket WHERE category_id = '${category}'`
);
return `${prefix}-${String(count + 1).padStart(3, '0')}`;
}
Ticket Detail
┌─────────────────────────────────────────────────────────────────┐
│ CHAR-015: NPC Bakker [🔙 Terug] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Status: 🟡 In Progress Priority: 🔴 Urgent │
│ Assigned to: designer@art.com Created: 2024-01-10 │
│ Due date: 2024-01-20 Category: Characters │
│ │
│ ───────────────────────────────────────────────────────────────│
│ │
│ Description: │
│ Vriendelijke bakker NPC voor het dorpsplein. │
│ Moet warm en uitnodigend overkomen. │
│ │
│ Style Notes: │
│ - Draagt wit schort met bloem │
│ - Ronde, vrolijke gezichtsvorm │
│ - Kleuren: warm bruin, crème, lichtgeel │
│ │
│ Specifications: │
│ Template: Character Sprite Sheet │
│ Size: 512 x 128 px │
│ Format: PNG (transparent) │
│ Frames: 4 (idle, walk x2, interact) │
│ │
│ References: │
│ [🖼️ ref1.jpg] [🖼️ ref2.jpg] │
│ │
│ ───────────────────────────────────────────────────────────────│
│ │
│ Uploads (2 versions): │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ v2 - baker_v2.png ✅ Approved │ │
│ │ 512x128 • 45KB • Uploaded Jan 18 │ │
│ │ [Preview] [Download] │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ v1 - baker_v1.png ⚠️ Revision │ │
│ │ 512x128 • 42KB • Uploaded Jan 15 │ │
│ │ Feedback: Schort moet witter, gezicht vriendelijker │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────────│
│ │
│ Comments: │
│ │
│ [Admin] Jan 10: Ticket aangemaakt │
│ [Admin] Jan 12: Toegewezen aan designer@art.com │
│ [Designer] Jan 15: Eerste versie geüpload │
│ [Admin] Jan 16: Graag schort witter maken │
│ [Designer] Jan 18: Aangepast, v2 geüpload │
│ [Admin] Jan 18: Goedgekeurd! ✅ │
│ │
│ [Commentaar toevoegen...] [Versturen] │
│ │
└─────────────────────────────────────────────────────────────────┘
File Upload
Upload naar Cloudflare R2
// lib/r2.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const r2 = new S3Client({
region: 'auto',
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY!,
secretAccessKey: process.env.R2_SECRET_KEY!,
},
});
export async function uploadAsset(
file: File,
ticketId: number,
version: number
): Promise<string> {
const key = `assets/tickets/${ticketId}/v${version}/${file.name}`;
await r2.send(new PutObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: key,
Body: Buffer.from(await file.arrayBuffer()),
ContentType: file.type,
}));
return `${process.env.R2_PUBLIC_URL}/${key}`;
}
Upload Component
function AssetUploader({ ticketId, onUpload }: Props) {
const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const handleUpload = async () => {
if (!file) return;
setUploading(true);
// Get next version number
const version = await getNextVersion(ticketId);
// Upload to R2
const url = await uploadAsset(file, ticketId, version);
// Get image dimensions
const dimensions = await getImageDimensions(file);
// Record in database
await spacetime.call('upload_asset_file', [
ticketId,
version,
file.name,
url,
file.size,
dimensions.width,
dimensions.height,
file.type,
'' // notes
]);
setUploading(false);
onUpload();
};
return (
<div>
<DropZone
accept={{ 'image/*': ['.png', '.svg'] }}
onDrop={files => setFile(files[0])}
/>
{file && (
<div>
<ImagePreview file={file} />
<Button
onClick={handleUpload}
disabled={uploading}
>
{uploading ? 'Uploaden...' : 'Upload'}
</Button>
</div>
)}
</div>
);
}
Spec Templates
Voorgedefinieerde specificaties per asset type:
| Template | Dimensies | Format | Notes |
|---|---|---|---|
| Character Sprite | 512x128 | PNG | 4 frames |
| Item Icon | 64x64 | PNG | Transparent |
| UI Button | 200x60 | SVG | 3 states |
| Background | 1920x1080 | PNG | - |
| Tileset | 256x256 | PNG | 16x16 tiles |
Designer Portal
Aparte view voor designers:
┌─────────────────────────────────────────────────────────────────┐
│ Mijn Tickets designer@art.com│
├─────────────────────────────────────────────────────────────────┤
│ │
│ Toegewezen aan mij (3) │
│ ─────────────────────── │
│ │
│ 🔴 CHAR-015 NPC Bakker Due: Jan 20 │
│ 🟡 ITEM-089 Gouden Schep Due: Jan 25 │
│ 🟢 MAP-003 Bos achtergrond Due: Feb 1 │
│ │
│ ───────────────────────────────────────────────────────────────│
│ │
│ Afgerond deze maand: 8 tickets │
│ Gemiddelde review tijd: 1.2 dagen │
│ │
└─────────────────────────────────────────────────────────────────┘