feat(content): add action costs and multi-yield schema
This commit is contained in:
@@ -3,7 +3,12 @@ import { buildContent } from '../schema';
|
|||||||
|
|
||||||
const validResources = [{ id: 'gold', name: 'Gold' }];
|
const validResources = [{ id: 'gold', name: 'Gold' }];
|
||||||
const validActions = [
|
const validActions = [
|
||||||
{ id: 'forage', name: 'Forage', durationMs: 3000, yields: { resourceId: 'gold', amount: 1 } },
|
{
|
||||||
|
id: 'forage',
|
||||||
|
name: 'Forage',
|
||||||
|
durationMs: 3000,
|
||||||
|
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('buildContent()', () => {
|
describe('buildContent()', () => {
|
||||||
@@ -24,7 +29,7 @@ describe('buildContent()', () => {
|
|||||||
id: 'forage',
|
id: 'forage',
|
||||||
name: 'Forage',
|
name: 'Forage',
|
||||||
durationMs: 3000,
|
durationMs: 3000,
|
||||||
yields: { resourceId: 'ghost', amount: 1 },
|
yields: [{ resourceId: 'ghost', amount: 1 }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(() => buildContent({ resources: validResources, actions })).toThrow(/unknown resource/i);
|
expect(() => buildContent({ resources: validResources, actions })).toThrow(/unknown resource/i);
|
||||||
@@ -40,8 +45,55 @@ describe('buildContent()', () => {
|
|||||||
|
|
||||||
it('rejects a structurally invalid definition', () => {
|
it('rejects a structurally invalid definition', () => {
|
||||||
const actions = [
|
const actions = [
|
||||||
{ id: 'forage', name: 'Forage', durationMs: -1, yields: { resourceId: 'gold', amount: 1 } },
|
{
|
||||||
|
id: 'forage',
|
||||||
|
name: 'Forage',
|
||||||
|
durationMs: -1,
|
||||||
|
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
expect(() => buildContent({ resources: validResources, actions })).toThrow();
|
expect(() => buildContent({ resources: validResources, actions })).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('costs and multi-yield', () => {
|
||||||
|
it('accepts optional costs and multiple yields', () => {
|
||||||
|
const resources = [
|
||||||
|
{ id: 'gold', name: 'Gold' },
|
||||||
|
{ id: 'wood', name: 'Wood' },
|
||||||
|
];
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
id: 'craft',
|
||||||
|
name: 'Craft',
|
||||||
|
durationMs: 1000,
|
||||||
|
costs: [{ resourceId: 'wood', amount: 2 }],
|
||||||
|
yields: [
|
||||||
|
{ resourceId: 'gold', amount: 1 },
|
||||||
|
{ resourceId: 'wood', amount: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const content = buildContent({ resources, actions });
|
||||||
|
expect(content.actionsById.craft.costs).toEqual([{ resourceId: 'wood', amount: 2 }]);
|
||||||
|
expect(content.actionsById.craft.yields).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects cost referencing unknown resource', () => {
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
id: 'craft',
|
||||||
|
name: 'Craft',
|
||||||
|
durationMs: 1000,
|
||||||
|
costs: [{ resourceId: 'ghost', amount: 1 }],
|
||||||
|
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(() => buildContent({ resources: validResources, actions })).toThrow(/unknown resource/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults costs to empty array', () => {
|
||||||
|
const content = buildContent({ resources: validResources, actions: validActions });
|
||||||
|
expect(content.actionsById.forage.costs).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export const actionDefs = [
|
|||||||
id: 'forage',
|
id: 'forage',
|
||||||
name: 'Forage for coin',
|
name: 'Forage for coin',
|
||||||
durationMs: 3000,
|
durationMs: 3000,
|
||||||
yields: { resourceId: 'gold', amount: 1 },
|
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
+21
-8
@@ -15,17 +15,21 @@ export const resourceDefSchema = z.object({
|
|||||||
startAmount: z.number().nonnegative().default(0),
|
startAmount: z.number().nonnegative().default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourceAmountSchema = z.object({
|
||||||
|
resourceId: z.string().min(1),
|
||||||
|
amount: z.number().positive(),
|
||||||
|
});
|
||||||
|
|
||||||
export const actionDefSchema = z.object({
|
export const actionDefSchema = z.object({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
durationMs: z.number().positive(),
|
durationMs: z.number().positive(),
|
||||||
yields: z.object({
|
costs: z.array(resourceAmountSchema).default([]),
|
||||||
resourceId: z.string().min(1),
|
yields: z.array(resourceAmountSchema).min(1),
|
||||||
amount: z.number().positive(),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ResourceDef = z.infer<typeof resourceDefSchema>;
|
export type ResourceDef = z.infer<typeof resourceDefSchema>;
|
||||||
|
export type ResourceAmount = z.infer<typeof resourceAmountSchema>;
|
||||||
export type ActionDef = z.infer<typeof actionDefSchema>;
|
export type ActionDef = z.infer<typeof actionDefSchema>;
|
||||||
|
|
||||||
export interface Content {
|
export interface Content {
|
||||||
@@ -55,10 +59,19 @@ export function buildContent(input: { resources: unknown[]; actions: unknown[] }
|
|||||||
const actionsById = indexById(actions, 'action');
|
const actionsById = indexById(actions, 'action');
|
||||||
|
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
if (!resourcesById[action.yields.resourceId]) {
|
for (const y of action.yields) {
|
||||||
throw new Error(
|
if (!resourcesById[y.resourceId]) {
|
||||||
`Action "${action.id}" yields unknown resource "${action.yields.resourceId}"`,
|
throw new Error(
|
||||||
);
|
`Action "${action.id}" yields unknown resource "${y.resourceId}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const c of action.costs) {
|
||||||
|
if (!resourcesById[c.resourceId]) {
|
||||||
|
throw new Error(
|
||||||
|
`Action "${action.id}" cost references unknown resource "${c.resourceId}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function testContent() {
|
|||||||
id: 'forage',
|
id: 'forage',
|
||||||
name: 'Forage',
|
name: 'Forage',
|
||||||
durationMs: 300,
|
durationMs: 300,
|
||||||
yields: { resourceId: 'gold', amount: 1 },
|
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ function testContent() {
|
|||||||
return buildContent({
|
return buildContent({
|
||||||
resources: [{ id: 'gold', name: 'Gold', startAmount: 5 }],
|
resources: [{ id: 'gold', name: 'Gold', startAmount: 5 }],
|
||||||
actions: [
|
actions: [
|
||||||
{ id: 'forage', name: 'Forage', durationMs: 300, yields: { resourceId: 'gold', amount: 2 } },
|
{ id: 'forage', name: 'Forage', durationMs: 300, yields: [{ resourceId: 'gold', amount: 2 }] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function testContent() {
|
|||||||
return buildContent({
|
return buildContent({
|
||||||
resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }],
|
resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }],
|
||||||
actions: [
|
actions: [
|
||||||
{ id: 'forage', name: 'Forage', durationMs: 3000, yields: { resourceId: 'gold', amount: 1 } },
|
{ id: 'forage', name: 'Forage', durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -50,6 +50,8 @@ export function tickGame(state: GameState, content: Content, tickMs: number): vo
|
|||||||
state.actionElapsedMs += tickMs;
|
state.actionElapsedMs += tickMs;
|
||||||
while (state.actionElapsedMs >= action.durationMs) {
|
while (state.actionElapsedMs >= action.durationMs) {
|
||||||
state.actionElapsedMs -= action.durationMs;
|
state.actionElapsedMs -= action.durationMs;
|
||||||
state.resources[action.yields.resourceId] += action.yields.amount;
|
for (const y of action.yields) {
|
||||||
|
state.resources[y.resourceId] += y.amount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function testContent() {
|
|||||||
return buildContent({
|
return buildContent({
|
||||||
resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }],
|
resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }],
|
||||||
actions: [
|
actions: [
|
||||||
{ id: 'forage', name: 'Forage', durationMs: 3000, yields: { resourceId: 'gold', amount: 1 } },
|
{ id: 'forage', name: 'Forage', durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function testContent() {
|
|||||||
return buildContent({
|
return buildContent({
|
||||||
resources: [{ id: 'gold', name: 'Gold', startAmount: 4 }],
|
resources: [{ id: 'gold', name: 'Gold', startAmount: 4 }],
|
||||||
actions: [
|
actions: [
|
||||||
{ id: 'forage', name: 'Forage', durationMs: 200, yields: { resourceId: 'gold', amount: 1 } },
|
{ id: 'forage', name: 'Forage', durationMs: 200, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user