NERDPOST: Content Frameworks in Cultist Simulator and Book of Hours

This is probably interesting to about 2% of the already select audience of this blog. Sorry, everyone else! But for those who are interested, this is from a Slack discussion about how the content framework in Book of Hours (which we’re calling ‘Vak’) looks like it’ll differently from the content framework in Cultist Simulator (retrospectively called ‘Fucine).

This is literally a format-tidied copy/paste (#opendevelopment!) so there are likely errors and typoes, and everything about Vak is very very subject to change.

so, Fucine, the current edition, does this:
this is a recipe

{ id: "studygreektutoring",
label: "Study Greek with a Tutor",
actionId: "study",
requirements: {tutorgreek:1,commission:-1,article:-1}, slots:[ {id:"Costs",label:"Costs",required: {compensationvalue: 1}}],
alternativerecipes: [{id:"studygreektutoringpaid",chance:100}], startdescription: "If I need to learn Greek, here's one who will help; but I'll need to pay in spintriae, the currency of the hidden world.",
description: "Tutors don't work for free. Another time.", warmup: 60,
craftable: true },


requirements: {tutorgreek:1,commission:-1,article:-1},
“If the tutorgreek aspect is in a study verb, and the commission and article aspects aren’t present, then present this recipe for execution” (edited)

slots:[ {id:"Costs",label:"Costs",required: {compensationvalue: 1}}],

“When the recipe runs, display a slot called ‘Costs’ which only accepts things with compensationvalue aspect”

alternativerecipes: [{id:"studygreektutoringpaid",chance:100}],

“While running this recipe, if the studygreektutoringpaid’s recipe’s requirements are fulfilled, switch to that instead”
{
id: "studygreektutoringpaid",
label: "Study Greek with a Tutor",
actionId: "study",
requirements: {compensationvalue:2},

so you can put anything with a compensationvalue in the slot, and the situation (the whole shebang of verb, recipe, contents) will execute the second recipe instead if it’s total compensation value >=2

so this is kinda
string TryStudyGreek(elements Elements)
{
if (Elements.Contains(“compensationvalue”,2)
return SucceedStudyGreek(Elements);
else
return “blah blah failed”;
}
but obviously insanely long-winded, for a good a bad and an ugly reason:

  • the ugly reason is that there are a bunch of UI concerns and effects that need to be addressed
  • the bad reason is when I first started building a recipe-based game, I didn’t realise how much business would need to be addressed by recipes switching to other recipes. I thought most interactions would just be one-and-done with a recipe
  • the good reason is that the recipes don’t really work like functions exactly, they’re more like instructions for a state machine, I think, which makes reasoning about how they work from a content design level much easier

Vak, on the other hand:
id: studygreektutoring
req: {study:1,tutorgreek:1,commission:0,article:0,start:1} preview: "Study Greek with a tutor", "If I need to learn Greek, here's one who will help; but I'll need to pay in spintriae, the currency of the hidden world."}
effects:
- slot: {id:assistance,label:assistance,allowed:{compensationvalue:1}}
- wait: 60
- seek: {studygreektutoringpaid}
- say: "Tutors don't work for free."

so that’s terser, obv. Using ‘study’ as another element rather than having a separate ‘actionid’ is more readable. The ‘start:1’ is the equivalent of ‘craftable:true’ – ie the player can kick it off, it’s not available only when switching recipes – and I’m not 100% happy with it but it’s more readable
a lot of the other changes are from expecting from the start that it won’t be one-and-done with recipes
eg: we now have an explicit ‘preview’ property, which means ‘display this text in relevant places in the UI when we’re predicting which recipes are candidates’. CS uses the same string for preview and startdescription, which is sometimes what we want but not always, especially when we’re dealing with recipes switching to recipes switching to recipes
eg: recipes have fewer explicit obligatory properties (‘startdescription’ ‘slot’ ‘warmup’) and more ‘things recipes can do in their list of effects (‘say:’ ‘slot:’ ‘wait:’). The biggest pain in writing Cultist content was trying to understand and remember how, for example, it would work if there was a recipe with a slot that linked to a recipe without a slot that linked to another recipe with a specific slot. So now we add and remove slots explicitly
- finishslot:assistance
although if a recipe just ends without transferring control to another recipe, any of its outstanding slots should go out of scope and disappear
one of the other big pains in Cultist was that I wanted a recipe to hand over control to other eligible recipes, but I needed to include all the eligible recipes in the list of alternativerecipes or linked
and I’d often write a recipe and forget to add it to the alt or linked, because the eligibility (the requirements) and the link from the previous recipe – that was sort of redundant
so now I want to be able to do
- seek: {studygreektutoringpaid,studygreektutoringunpaid,studygreekinsultedtutor,studygreekthisgrammaristoohard} <– semantically the same as alternativerecipes: [{id:"studygreektutorpaid"}, {id:"studygreektutorunpaid"}.... etc
but also
- seek: {studygreek} where ‘studygreek’ is a module that contains all the recipes where we care about eligibility
and indeed
- seek: {} to find any eligible recipe in the whole content base
finally:

Situations, and recipe eligibility

A situation is not a VAK element: it’s an instance of an executing recipe. To put it another way, a situation is the scope in which a recipe executes, and to which elements are limited until they’re ejected or the situation ends.

A situation can only be executing one recipe at a time, but it pass control to another recipe to continue executing. So a situation consists of one or more recipes executing in series.

Recipes can be executed in two ways

(1) when a player attempts to begin a new situation by combining elements and activates ‘Start’

(2) when a recipe uses seek: to surrender control to another recipe without ending the situation.

  1. Hey, this was interesting af, you got at least one person liking this. Do post more if you feel like it.