feat(content): add action kind and group schema
This commit is contained in:
@@ -6,6 +6,7 @@ const validActions = [
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
durationMs: 3000,
|
||||
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||
},
|
||||
@@ -28,6 +29,7 @@ describe('buildContent()', () => {
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
durationMs: 3000,
|
||||
yields: [{ resourceId: 'ghost', amount: 1 }],
|
||||
},
|
||||
@@ -48,6 +50,7 @@ describe('buildContent()', () => {
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
durationMs: -1,
|
||||
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||
},
|
||||
@@ -66,6 +69,7 @@ describe('costs and multi-yield', () => {
|
||||
{
|
||||
id: 'craft',
|
||||
name: 'Craft',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
durationMs: 1000,
|
||||
costs: [{ resourceId: 'wood', amount: 2 }],
|
||||
yields: [
|
||||
@@ -84,6 +88,7 @@ describe('costs and multi-yield', () => {
|
||||
{
|
||||
id: 'craft',
|
||||
name: 'Craft',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
durationMs: 1000,
|
||||
costs: [{ resourceId: 'ghost', amount: 1 }],
|
||||
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||
@@ -104,6 +109,7 @@ describe('unlock conditions', () => {
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
durationMs: 3000,
|
||||
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||
unlock: {
|
||||
@@ -131,6 +137,7 @@ describe('action narrative fields', () => {
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
durationMs: 3000,
|
||||
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||
storyHint: 'Gather what the forest offers.',
|
||||
@@ -148,3 +155,57 @@ describe('action narrative fields', () => {
|
||||
expect(content.actionsById.forage.storyTooltip).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('action kind schema', () => {
|
||||
it('accepts kind, group, and storyChoiceId', () => {
|
||||
const content = buildContent({
|
||||
resources: [{ id: 'supplies', name: 'Supplies' }],
|
||||
actions: [
|
||||
{
|
||||
id: 'pick_high_road',
|
||||
name: 'Take the high road',
|
||||
kind: 'story',
|
||||
group: { id: 'fork', label: 'Crossroads' },
|
||||
storyChoiceId: 'pick_a',
|
||||
yields: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(content.actionsById.pick_high_road.kind).toBe('story');
|
||||
expect(content.actionsById.pick_high_road.group.label).toBe('Crossroads');
|
||||
});
|
||||
|
||||
it('defaults kind to timed and requires durationMs for timed actions', () => {
|
||||
expect(() =>
|
||||
buildContent({
|
||||
resources: [{ id: 'supplies', name: 'Supplies' }],
|
||||
actions: [
|
||||
{
|
||||
id: 'broken',
|
||||
name: 'Broken',
|
||||
kind: 'timed',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
yields: [{ resourceId: 'supplies', amount: 1 }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('requires durationMs for loop actions', () => {
|
||||
const content = buildContent({
|
||||
resources: [{ id: 'supplies', name: 'Supplies' }],
|
||||
actions: [
|
||||
{
|
||||
id: 'rest',
|
||||
name: 'Rest',
|
||||
kind: 'loop',
|
||||
group: { id: 'camp_loop', label: 'Camp activities' },
|
||||
durationMs: 2000,
|
||||
yields: [{ resourceId: 'supplies', amount: 1 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(content.actionsById.rest.kind).toBe('loop');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,4 +9,4 @@ const story = buildStoryContent(storyNodeDefs, base.actionsById, base.resourcesB
|
||||
export type GameContent = Content & StoryContent;
|
||||
export const content: GameContent = { ...base, ...story };
|
||||
|
||||
export type { ActionDef, Content, ResourceDef } from './schema';
|
||||
export type { ActionDef, ActionGroup, ActionKind, Content, ResourceDef } from './schema';
|
||||
|
||||
+46
-3
@@ -25,20 +25,63 @@ export const unlockDefSchema = z.object({
|
||||
requireStoryFlags: z.array(z.string().min(1)).optional(),
|
||||
});
|
||||
|
||||
export const actionDefSchema = z.object({
|
||||
export const actionKindSchema = z.enum(['instant', 'loop', 'timed', 'story', 'context']);
|
||||
|
||||
export const actionGroupSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
label: z.string().min(1),
|
||||
});
|
||||
|
||||
export const actionDefSchema = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
durationMs: z.number().positive(),
|
||||
kind: actionKindSchema.default('timed'),
|
||||
group: actionGroupSchema,
|
||||
durationMs: z.number().positive().optional(),
|
||||
loopPriority: z.number().int().nonnegative().optional(),
|
||||
costs: z.array(resourceAmountSchema).default([]),
|
||||
yields: z.array(resourceAmountSchema).min(1),
|
||||
yields: z.array(resourceAmountSchema).default([]),
|
||||
unlock: unlockDefSchema.optional(),
|
||||
storyHint: z.string().min(1).optional(),
|
||||
storyTooltip: z.string().min(1).optional(),
|
||||
storyChoiceId: z.string().min(1).optional(),
|
||||
contextId: z.string().min(1).optional(),
|
||||
automation: z
|
||||
.object({
|
||||
unlockAfterManualCompletions: z.number().int().positive().default(1),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.superRefine((action, ctx) => {
|
||||
if ((action.kind === 'timed' || action.kind === 'loop') && action.durationMs === undefined) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: `${action.kind} actions require durationMs`,
|
||||
path: ['durationMs'],
|
||||
});
|
||||
}
|
||||
if (action.kind === 'story' && !action.storyChoiceId) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'story actions require storyChoiceId',
|
||||
path: ['storyChoiceId'],
|
||||
});
|
||||
}
|
||||
if (action.kind === 'timed' && action.yields.length === 0) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'timed actions require at least one yield',
|
||||
path: ['yields'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type ResourceDef = z.infer<typeof resourceDefSchema>;
|
||||
export type ResourceAmount = z.infer<typeof resourceAmountSchema>;
|
||||
export type UnlockDef = z.infer<typeof unlockDefSchema>;
|
||||
export type ActionKind = z.infer<typeof actionKindSchema>;
|
||||
export type ActionGroup = z.infer<typeof actionGroupSchema>;
|
||||
export type ActionDef = z.infer<typeof actionDefSchema>;
|
||||
|
||||
export interface Content {
|
||||
|
||||
Reference in New Issue
Block a user