Tote CCs, Entzug und Gruppen-CCs

Bisher entschied allein das Redesign darueber, ob eine Rueckkehr etwas kostet.
Es fehlten zwei Faelle: ein Entzug ohne Anspruch auf kostenlose Rueckkehr, und
der Tod im Rollenspiel. Ein toter CC laesst sich weder gratis noch gegen
Guthaben auf dem normalen Weg zurueckholen - das entscheidet ein Administrator
ueber einen eigenen Weg, mit Pflicht-Begruendung, und waehlt dabei, ob ein
Guthaben fliesst. Schon das Eintragen des Todes verlangt eine Notiz.

Dazu koennen sich mehrere Spieler einen CC teilen. Die Zugehoerigkeit laeuft
ab jetzt durchgehend ueber eine eigene Tabelle, auch bei Einzel-CCs; die
Migration traegt bestehende CCs als erstes Mitglied ihres Erstellers nach.
Ohne das waere nach dem Update kein CC mehr beim Spieler aufgetaucht.

Fuer jeden Mitnutzer gelten dieselben Regeln wie fuer einen eigenen CC: der
Platz kostet ein eigenes Guthaben, wahlweise aus dem CC-Paket oder ueber ein
Redesign. Ein Austritt gibt nichts zurueck, denn bis dahin wurde der CC
genutzt; fuer eine Fehleingabe ist das Storno der richtige Weg. Der letzte
Nutzer laesst sich nicht austragen - dann wird der CC deaktiviert. Ein
Redesign nimmt die Gruppe mit.
This commit is contained in:
2026-09-20 23:03:22 +02:00
parent 3a55ef1b2f
commit e78a57e35e
11 changed files with 1193 additions and 53 deletions
+59 -2
View File
@@ -1,10 +1,14 @@
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { CreditKind, InactiveReason } from '@prisma/client';
import { requireEditor } from '../auth/guard.js';
import { requireAdmin, requireAuth, requireEditor } from '../auth/guard.js';
import {
addMember,
createCharacter,
deactivateCharacter,
listMembers,
removeMember,
reviveDeadCharacter,
reactivateFree,
reactivatePaid,
redesignCharacter,
@@ -88,7 +92,12 @@ export async function characterRoutes(app: FastifyInstance): Promise<void> {
const { id } = parseWith(idParam, request.params);
const body = parseWith(
z.object({
reason: z.enum([InactiveReason.PLAYER_INACTIVITY, InactiveReason.OTHER]),
reason: z.enum([
InactiveReason.PLAYER_INACTIVITY,
InactiveReason.OTHER,
InactiveReason.NO_FREE_CLAIM,
InactiveReason.DEAD,
]),
note: optionalNote,
}),
request.body,
@@ -110,6 +119,54 @@ export async function characterRoutes(app: FastifyInstance): Promise<void> {
return reactivatePaid(id, body.useKind, contextOf(request), body.note);
});
/**
* Rückkehr eines im Rollenspiel gestorbenen CCs. Ausnahmeentscheidung,
* deshalb Administratoren vorbehalten und nur mit Begründung.
*/
app.post('/api/characters/:id/revive-dead', { preHandler: requireAdmin }, async (request) => {
const { id } = parseWith(idParam, request.params);
const body = parseWith(
z.object({
reason: z.string().trim().min(1, 'Eine Begründung ist Pflicht').max(500),
useKind: reviveKind.optional(),
}),
request.body,
);
return reviveDeadCharacter(id, body, contextOf(request));
});
// --- Gruppen-CCs ---
app.get('/api/characters/:id/members', { preHandler: requireAuth }, async (request) => {
const { id } = parseWith(idParam, request.params);
return { items: await listMembers(id) };
});
app.post('/api/characters/:id/members', { preHandler: requireEditor }, async (request) => {
const { id } = parseWith(idParam, request.params);
const body = parseWith(
z.object({
playerId: z.number().int().positive(),
useKind: z.enum([CreditKind.CC, CreditKind.REDESIGN_PAID, CreditKind.REDESIGN_KULANZ]),
note: optionalNote,
}),
request.body,
);
return addMember(id, body.playerId, { useKind: body.useKind, note: body.note }, contextOf(request));
});
app.delete('/api/characters/:id/members/:playerId', { preHandler: requireEditor }, async (request) => {
const params = parseWith(
z.object({
id: z.coerce.number().int().positive(),
playerId: z.coerce.number().int().positive(),
}),
request.params,
);
const body = parseWith(z.object({ note: optionalNote }), request.body ?? {});
return removeMember(params.id, params.playerId, body.note, contextOf(request));
});
app.post('/api/characters/:id/revert', { preHandler: requireEditor }, async (request) => {
const { id } = parseWith(idParam, request.params);
const body = parseWith(