Add Zod dependency and update API interfaces
- Added Zod as a dependency in package.json. - Updated pnpm-lock.yaml to include Zod. - Refactored API interfaces: exported new modules for perk, survivor, mission, and encounter. - Removed obsolete api-interfaces.ts file. - Enhanced tests for new schemas in api-interfaces.spec.ts, covering various validation scenarios.
This commit is contained in:
@@ -1 +1,4 @@
|
||||
export * from './lib/api-interfaces';
|
||||
export * from './lib/perk';
|
||||
export * from './lib/survivor';
|
||||
export * from './lib/mission';
|
||||
export * from './lib/encounter';
|
||||
|
||||
@@ -1,7 +1,199 @@
|
||||
import { apiInterfaces } from './api-interfaces';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
EncounterResultSchema,
|
||||
MissionParticipantSchema,
|
||||
MissionSchema,
|
||||
PerkModifierSchema,
|
||||
PerkSchema,
|
||||
SurvivorSchema,
|
||||
SurvivorStatsSchema,
|
||||
SurvivorStateSchema,
|
||||
} from '../index';
|
||||
|
||||
describe('apiInterfaces', () => {
|
||||
it('should work', () => {
|
||||
expect(apiInterfaces()).toEqual('api-interfaces');
|
||||
const PERK_ID = 'a0000000-0000-4000-8000-000000000001';
|
||||
const SURVIVOR_ID = 'a0000000-0000-4000-8000-000000000002';
|
||||
const MISSION_ID = 'a0000000-0000-4000-8000-000000000003';
|
||||
|
||||
const validPerk = {
|
||||
id: PERK_ID,
|
||||
key: 'borrowed_time',
|
||||
name: 'Borrowed Time',
|
||||
description: 'Extends the window for decisive action.',
|
||||
tags: ['rescue', 'altruism'],
|
||||
modifiers: [
|
||||
{ target: 'altruism', type: 'additive', amount: 2 },
|
||||
{
|
||||
target: 'successChance',
|
||||
type: 'multiplicative',
|
||||
amount: 1.1,
|
||||
condition: { encounterTags: ['hook'] },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const validSurvivor = {
|
||||
id: SURVIVOR_ID,
|
||||
opaqueUserId: 'U123456',
|
||||
channelId: 'ch_001',
|
||||
name: 'Ada',
|
||||
state: 'active',
|
||||
stats: { objectives: 7, survival: 5, altruism: 8 },
|
||||
perkSlots: [validPerk],
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
const validMission = {
|
||||
id: MISSION_ID,
|
||||
groupId: null,
|
||||
participants: [
|
||||
{
|
||||
survivorId: SURVIVOR_ID,
|
||||
state: 'active',
|
||||
hookCount: 0,
|
||||
},
|
||||
],
|
||||
difficulty: 2,
|
||||
status: 'active',
|
||||
encounterLibraryVersion: '1.0.0',
|
||||
nextTickAt: '2026-01-01T00:01:00.000Z',
|
||||
tickIndex: 0,
|
||||
startedAt: '2026-01-01T00:00:00.000Z',
|
||||
endedAt: null,
|
||||
};
|
||||
|
||||
describe('SurvivorStateSchema', () => {
|
||||
it('accepts all valid states', () => {
|
||||
for (const s of ['idle', 'active', 'injured', 'downed', 'sacrificed']) {
|
||||
expect(SurvivorStateSchema.parse(s)).toBe(s);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects unknown states', () => {
|
||||
expect(() => SurvivorStateSchema.parse('dead')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SurvivorStatsSchema', () => {
|
||||
it('rejects stats outside 1–10', () => {
|
||||
expect(() =>
|
||||
SurvivorStatsSchema.parse({ objectives: 0, survival: 5, altruism: 5 })
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
SurvivorStatsSchema.parse({ objectives: 5, survival: 11, altruism: 5 })
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PerkModifierSchema', () => {
|
||||
it('accepts a conditional modifier', () => {
|
||||
const result = PerkModifierSchema.parse(validPerk.modifiers[1]);
|
||||
expect(result.condition?.encounterTags).toEqual(['hook']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PerkSchema', () => {
|
||||
it('parses a valid perk', () => {
|
||||
expect(PerkSchema.parse(validPerk).key).toBe('borrowed_time');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SurvivorSchema', () => {
|
||||
it('parses a valid survivor', () => {
|
||||
expect(SurvivorSchema.parse(validSurvivor).name).toBe('Ada');
|
||||
});
|
||||
|
||||
it('rejects anonymous opaque user IDs', () => {
|
||||
expect(() =>
|
||||
SurvivorSchema.parse({ ...validSurvivor, opaqueUserId: 'A999' })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('rejects more than 4 perk slots', () => {
|
||||
expect(() =>
|
||||
SurvivorSchema.parse({
|
||||
...validSurvivor,
|
||||
perkSlots: [validPerk, validPerk, validPerk, validPerk, validPerk],
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MissionParticipantSchema', () => {
|
||||
it('rejects hookCount > 2', () => {
|
||||
expect(() =>
|
||||
MissionParticipantSchema.parse({
|
||||
survivorId: SURVIVOR_ID,
|
||||
state: 'downed',
|
||||
hookCount: 3,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MissionSchema', () => {
|
||||
it('parses a valid solo mission', () => {
|
||||
expect(MissionSchema.parse(validMission).difficulty).toBe(2);
|
||||
});
|
||||
|
||||
it('rejects difficulty outside 1–3', () => {
|
||||
expect(() =>
|
||||
MissionSchema.parse({ ...validMission, difficulty: 4 })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('rejects groups larger than 4', () => {
|
||||
const participant = validMission.participants[0];
|
||||
expect(() =>
|
||||
MissionSchema.parse({
|
||||
...validMission,
|
||||
participants: [
|
||||
participant,
|
||||
participant,
|
||||
participant,
|
||||
participant,
|
||||
participant,
|
||||
],
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EncounterResultSchema', () => {
|
||||
it('parses a successful encounter with no state change', () => {
|
||||
const result = EncounterResultSchema.parse({
|
||||
missionId: MISSION_ID,
|
||||
survivorId: SURVIVOR_ID,
|
||||
encounterKey: 'cleanse_hex',
|
||||
tickIndex: 1,
|
||||
seed: 'abc123',
|
||||
success: true,
|
||||
survivorStateChange: null,
|
||||
modifiersApplied: [],
|
||||
logText: 'Ada cleanses the hex totem. The fog thins briefly.',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.survivorStateChange).toBeNull();
|
||||
});
|
||||
|
||||
it('parses an encounter that injures the survivor', () => {
|
||||
const result = EncounterResultSchema.parse({
|
||||
missionId: MISSION_ID,
|
||||
survivorId: SURVIVOR_ID,
|
||||
encounterKey: 'stalked',
|
||||
tickIndex: 2,
|
||||
seed: 'def456',
|
||||
success: false,
|
||||
survivorStateChange: { from: 'active', to: 'injured' },
|
||||
modifiersApplied: [
|
||||
{
|
||||
perkKey: 'borrowed_time',
|
||||
target: 'survival',
|
||||
type: 'additive',
|
||||
effectiveAmount: 2,
|
||||
},
|
||||
],
|
||||
logText: 'Something moves in the trees. Ada takes a hit.',
|
||||
});
|
||||
expect(result.survivorStateChange?.to).toBe('injured');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function apiInterfaces(): string {
|
||||
return 'api-interfaces';
|
||||
}
|
||||
29
libs/api-interfaces/src/lib/encounter.ts
Normal file
29
libs/api-interfaces/src/lib/encounter.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { z } from 'zod';
|
||||
import { PerkModifierTargetSchema, PerkModifierTypeSchema } from './perk';
|
||||
import { SurvivorStateSchema } from './survivor';
|
||||
|
||||
export const ModifierApplicationSchema = z.object({
|
||||
perkKey: z.string().min(1),
|
||||
target: PerkModifierTargetSchema,
|
||||
type: PerkModifierTypeSchema,
|
||||
effectiveAmount: z.number(),
|
||||
});
|
||||
export type ModifierApplication = z.infer<typeof ModifierApplicationSchema>;
|
||||
|
||||
export const EncounterResultSchema = z.object({
|
||||
missionId: z.uuid(),
|
||||
survivorId: z.uuid(),
|
||||
encounterKey: z.string().min(1),
|
||||
tickIndex: z.number().int().min(0),
|
||||
seed: z.string().min(1),
|
||||
success: z.boolean(),
|
||||
survivorStateChange: z
|
||||
.object({
|
||||
from: SurvivorStateSchema,
|
||||
to: SurvivorStateSchema,
|
||||
})
|
||||
.nullable(),
|
||||
modifiersApplied: z.array(ModifierApplicationSchema),
|
||||
logText: z.string().min(1),
|
||||
});
|
||||
export type EncounterResult = z.infer<typeof EncounterResultSchema>;
|
||||
32
libs/api-interfaces/src/lib/mission.ts
Normal file
32
libs/api-interfaces/src/lib/mission.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
import { SurvivorStateSchema } from './survivor';
|
||||
|
||||
export const MissionStateSchema = z.enum([
|
||||
'lobby',
|
||||
'active',
|
||||
'success',
|
||||
'sacrifice',
|
||||
'abandoned',
|
||||
]);
|
||||
export type MissionState = z.infer<typeof MissionStateSchema>;
|
||||
|
||||
export const MissionParticipantSchema = z.object({
|
||||
survivorId: z.uuid(),
|
||||
state: SurvivorStateSchema,
|
||||
hookCount: z.number().int().min(0).max(2),
|
||||
});
|
||||
export type MissionParticipant = z.infer<typeof MissionParticipantSchema>;
|
||||
|
||||
export const MissionSchema = z.object({
|
||||
id: z.uuid(),
|
||||
groupId: z.uuid().nullable(),
|
||||
participants: z.array(MissionParticipantSchema).min(1).max(4),
|
||||
difficulty: z.number().int().min(1).max(3),
|
||||
status: MissionStateSchema,
|
||||
encounterLibraryVersion: z.string().min(1),
|
||||
nextTickAt: z.iso.datetime(),
|
||||
tickIndex: z.number().int().min(0),
|
||||
startedAt: z.iso.datetime(),
|
||||
endedAt: z.iso.datetime().nullable(),
|
||||
});
|
||||
export type Mission = z.infer<typeof MissionSchema>;
|
||||
34
libs/api-interfaces/src/lib/perk.ts
Normal file
34
libs/api-interfaces/src/lib/perk.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PerkModifierTargetSchema = z.enum([
|
||||
'successChance',
|
||||
'objectives',
|
||||
'survival',
|
||||
'altruism',
|
||||
]);
|
||||
export type PerkModifierTarget = z.infer<typeof PerkModifierTargetSchema>;
|
||||
|
||||
export const PerkModifierTypeSchema = z.enum(['additive', 'multiplicative']);
|
||||
export type PerkModifierType = z.infer<typeof PerkModifierTypeSchema>;
|
||||
|
||||
export const PerkModifierSchema = z.object({
|
||||
target: PerkModifierTargetSchema,
|
||||
type: PerkModifierTypeSchema,
|
||||
amount: z.number(),
|
||||
condition: z
|
||||
.object({
|
||||
encounterTags: z.array(z.string()).min(1),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
export type PerkModifier = z.infer<typeof PerkModifierSchema>;
|
||||
|
||||
export const PerkSchema = z.object({
|
||||
id: z.uuid(),
|
||||
key: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
description: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
modifiers: z.array(PerkModifierSchema),
|
||||
});
|
||||
export type Perk = z.infer<typeof PerkSchema>;
|
||||
30
libs/api-interfaces/src/lib/survivor.ts
Normal file
30
libs/api-interfaces/src/lib/survivor.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { z } from 'zod';
|
||||
import { PerkSchema } from './perk';
|
||||
|
||||
export const SurvivorStateSchema = z.enum([
|
||||
'idle',
|
||||
'active',
|
||||
'injured',
|
||||
'downed',
|
||||
'sacrificed',
|
||||
]);
|
||||
export type SurvivorState = z.infer<typeof SurvivorStateSchema>;
|
||||
|
||||
export const SurvivorStatsSchema = z.object({
|
||||
objectives: z.number().int().min(1).max(10),
|
||||
survival: z.number().int().min(1).max(10),
|
||||
altruism: z.number().int().min(1).max(10),
|
||||
});
|
||||
export type SurvivorStats = z.infer<typeof SurvivorStatsSchema>;
|
||||
|
||||
export const SurvivorSchema = z.object({
|
||||
id: z.uuid(),
|
||||
opaqueUserId: z.string().regex(/^U/, 'Must be a logged-in viewer opaque user ID'),
|
||||
channelId: z.string().min(1),
|
||||
name: z.string().min(1).max(32),
|
||||
state: SurvivorStateSchema,
|
||||
stats: SurvivorStatsSchema,
|
||||
perkSlots: z.array(PerkSchema).max(4),
|
||||
createdAt: z.iso.datetime(),
|
||||
});
|
||||
export type Survivor = z.infer<typeof SurvivorSchema>;
|
||||
Reference in New Issue
Block a user