I'm BrunoDias, a São Paulo-based designer (systems, narrative, mechanics) and writer working in video games.Azhdarchid (named after the family of large pterosaurs) is my personal blog; I also have a more professional landing page at brunodias.dev.
You may know me from such video games as Where the Water Tastes Like Wine, Neo Cab, Pathologic 2 and 3, or Fallen London—you can see my full credits on MobyGames. I also edit the Game Narrative Reader, a link anthology of great essays about video game narrative design on the web.
I think this hits very different if it comes out even one year ago. In this environment it feels like what you're buying is a ticket for two hours of respite for slop world (so you can walk out and immediately find out that Letterboxd might be bought by the fascist new york times, in which case this review, and everything else on my account, will be promptly deleted from the website. sorry. I syndicate them all to my blog.)
I think Laika knows this; at the very end of the credits sequence, they show just a few brief shots of the process. One of a puppet's outfits being carefully sewn by hand; seemingly endless racks of minutely different faces that they 3D-printed out to animate Prue's facial expressions; the faint ghost of an animator moving with time-lapse speed, days of work to create seconds of footage. It feels pointed. It feels like the people who made this movie looking across the screen at me and going "yeah, we know."
It's a very hard movie to analyze, I think. I could sit here and pick this script apart, although I wonder how much of the impulse there is less about the screenplay having problems and more about it just not being as thematically tidy as you kind of expect a kid's movie to be. It is a kid's movie, thematically and structurally, but it's also way more dark and violent than a typical kid's movie (complete with PG-13 rating).
It's a kind of movie we don't see so much these days, the kid's movie that—if you watch it at just the right age—will rewire a child's entire brain. This is going to be some kid's Secret of Nihm or Last Unicorn in a way that I don't think Laika's previous work would quite have hit, precisely because it is so dark and violent. It does have that Coraline quality of being a movie for children about how your parents are, inevitably, flawed people who'll disappoint you.
Visually, of course, there is nothing else that comes close to this and there won't be... probably until Laika makes another movie, which I hope they do; I hope this movie makes a gazillion dollars. This is a movie featuring what I can only describe as the stop-motion Battle of Helm's Deep. It is constantly pushing the boundaries of what you think stop motion could do, then subtly reminding you that you are watching puppets get moved about on a one-eighth-scale world. Laika's approach to stop motion is so unbelievably refined now that it feels kind of warm and fuzzy when you spot just a little tiny bit of that staccato quality you normally expect.
This was probably one of the most valuable experiences I had in a theater all year, so the fact that I almost don't want to rate it that highly "as a movie" in some abstracted, ethereal sense is perhaps more a me problem than a problem with this movie. I think you should see this in a theater. I feel like the term "masterpiece" gets thrown around too casually but this is about as much of a masterpiece, in the very literal sense that it's a demonstration of surpassing mastery of a craft, as anything you'll see all year.
On recognizing the battle line when it appears outside your window.
A little over a week ago, on the night of the Brazilian election runoff, I spent several hours going slowly from despair, to cautious hope, to a sort of weary joy. I live, these days, in a downtown area of São Paulo. I generally hate crowds, but on that night I did walk the length of Avenida Paulista, the city's de facto public square, where people were gathering—at first just to celebrate, and later on to hear president Lula's victory speech, which he delivered in front of the iconic Modernist hulk of the São Paulo Museum of Art.
Ever since I have been periodically reminded of everything that was under threat in the event of another Bolsonaro government, of the losses we couldn't afford that now look to be averted, and of the effort needed to get this outcome.
And I'm also periodically reminded of who was on the other side of this effort; an unpleasant alliance of fascists and criminals who anticipated breaking the nation down for parts. And fighting right alongside them: Elon Musk and Mark Zuckerberg. Meta, in particular, directly intervened in suppressing pro-Lula messaging on Instagram while artificially boosting pro-Bolsonaro messaging.
Big tech behaves as a political actor, and they support fascists. Google and Amazon supply cloud infrastructure that Israel's government and military uses in the prosecution of their ongoing genocide against the Palestinian people, as does Microsoft. Tech CEOs have made repeated shows of support and even subservience to the Trump administration.
These companies are embedded in all of our lives, but at the same time they have to be understood as hostile and categorically evil entities. At present, they are all heavily involved in the AI push, which is its own novel form of mass harm that abuts, but isn't exactly the same as, the other forms of harm that these corporations are involved in.
I really do believe at this point that AI is going to be the most harmful new technology of the 21st century. If it is not reined in, it is our time's version of leaded gasoline: ubiquitous and ubiquitously poisonous, a massive corporate assault on the common welfare of human beings and the planet.
Some of those harms are inherent to the technology; I do not believe that it is plausibly safe for everyone to be in conversation with an LLM chatbot regularly, and I've found people who are have their judgment become severely questionable very rapidly. I don't think it is a positive development for mathematicians to be pulling the lever on a counterexample slot machine that precludes actual development of new techniques or the personal development of new mathematicians. I don't think the rapid, thoughtless generation of large volumes of source code that nobody has ever read is likely to be positive for software.
The tech, in itself, will always be more adept at generating spam than useful output, will always be more useful to spammers and grifters than to well-meaning actors. It is a semiotic pollutant that attacks the foundations of social and epistemological trust. These systems should not exist. They should not be deployed, they should not be used, by anyone, for any purpose.
But beyond the inherent harmful nature of the tech, the social system that promotes and builds up these systems is itself engaged in mass harm, independently of what LLM systems themselves do. Hyperscalers are standing up new gas turbine capacity that, if they have their way, will set back the US' emissions targets by a decade. The data center build-up is an environmental, labor, and economic disaster. It is sucking the air out of everything else—literally the US would be in recession right now if not for data center construction.
The credit crunch is hitting other venture capital intensive industries like video games; the main impact of AI on the games industry are that no one can afford RAM and there's no funding to make video games. Microsoft's pivot to AI alone is responsible for thousands of jobs lost and the destruction of several important and storied game studios.
All of those harms and all of those forces are, fundamentally, connected. Big tech is a force for disinformation, for the promotion of fascism, for the ruination of our cultural environment and also our literal environment. They have chosen a side, and it's not the side of humanity.
What last month clarified for me is that we are not engaged in the normal to and fro of politics, we are in a hybrid conflict against a loosely-defined but very much unified set of forces: tech corporations, fossil fuels, and international fascism arrayed together. They can be fought, and they can be beaten, but it falls on all of us to understand that this is a conflict and there are sides.
Whether you think of it in these terms or not, whether you like it or not, by virtue of being a human being you are in conflict with this adversary, because this adversary has put itself in conflict with humanity. The most basic and universal values are under assault, as is our ability to even articulate those values, as is our shared conception of reality, as is the biosphere itself.
It is fundamental to not understand the current moment as just the adoption shock of a new technology. The technology itself is harmful; but the tech corporations pushing it want to ensure it is an engine of dependency, surveillance, and monopoly.
The people who promote this technology, the people who deploy this technology, are themselves taking a side. It is not a neutral act or a matter of personal convenience. It's signing up to fight, and to fight on the side of those who very nearly knocked of Brazilian democracy a week and a half ago; on the side of new gas turbines; on the side of everything being choked with slop; on the side of total semiotic dissolution.
The actual prompt for this post is that I learned that a friend—or rather, someone I used to respect and consider a friend—has recently taken a job with the AI division of a major tech corporation.
I feel like a lot of us are currently watching people we know fail a moral test, and it can drive one insane to feel like what seems like a clear moral line is a "personal choice" to others. To see people delude themselves with self-serving narratives like "I can steer the way this technology develops from the inside" or "I need to confront what it is head on."
But I wanted to write this specifically to say to other people in this position: No, you're not insane. We are living through a horrific break in human history, and we are going to watch people fail to recognize the battle lines being drawn or simply fail a basic decency check. We are watching a mass betrayal of humanistic values unfold before our eyes. It is natural to be shocked and disgusted.
What it feels like is a civil war; a conflict where we find ourselves opposing people we used to take seriously or even cared about. In video games this has become very stark; big-name indie developers becoming various shades of AI chud while half the people I know in the industry are unemployed and the other half are miserable because of their AI-pilled bosses.
I don't know what the outcome of all this will be, other than I know it will be destructive. The world is simply worse for these systems existing, no matter how many insane bad-faith conversations about "utility" or "capability" you get roped into. It is exhausting and it is miserable to watch.
But it is also true that last month brought the realization that those corporations can be fought, and they can be beaten.
Halloween, 1957. Josephine Sable has a brand-new doctorate in high energy physics, several problems to solve, and no job.
For ECTOCOMP 2026, I am releasing a new, small, free game: First, Second, and Third Persons. A short piece of interactive horror-comedy about death, the stupid side of dark academia, and that feeling one sometimes gets that one has gone to the wrong party, and would very much like to kill everyone in attendance.
Lyre is a narrative scripting language built designed to build story worlds with complex mechanics, highly variable state, and the ability to grow organically.
A little under ten years ago (dear god) I tried, and eventually gave up on, building what I called at the time a "general-purpose QBN system". That is to say, a system for storylet narrative that could be used as narrative middleware or as a narrative scripting engine, like Ink or Yarn Spinner.
A lot has happened in the intervening time, and I've come to this problem with a different perspective. I now think this is feasible. It's so feasible, in fact, that I've done it.
What changed? Well...
I spent four years working with Failbetter Games' StoryNexus system, which back in 2017 was already not available to the general public. I wrote that original article having just shipped Voyageur, of course, which is significantly inspired by StoryNexus; but between then and now I've written an order of magnitude more material intended for this kind of narrative, and I have a much clearer picture of the kinds of affordances I want.
Yarn Spinner's model of storylet narrative (what it calls node groups) usefully solves for a lot of storylet narrative use cases, which effectively narrows what I'm trying to do to the use cases that Yarn isn't particularly suited to. I feel happier building something opinionated for a narrow use case.
The overall success of both Ink and Yarn led me to conclude that a middleware-style DSL tool was the way to go, rather than something with an obligate GUI.
I simply decided to level up my technical skills to the point where I could do it.
The result is lyre, a new narrative scripting language designed specifically for building storylet narratives in the vein of games like Sunless Sea,Voyageur,Fallen London, and the various third-party games that used to be available on StoryNexus.
Now, to be clear: I am not releasing lyre yet. I plan for a first public release sometime later this year, after the release of the first game made with it (which should be a short piece of IF and should be out, hopefully, at the end of the month). This post is a preview, though the lyre compiler and runtime are already working software. I'm writing this mostly to start articulating some of the design choices and intentions while it's still all fresh in my mind; I'll probably write more about it down the line when it's out.
"Storylet narrative" is a very broad term that can include everything from Citizen Sleeper, to Sunless Skies, to the event system in a Paradox game. The specific concept lyre is built for is a narrower and more specific design, where...
Game state is represented entirely[1] in terms of qualities. Qualities are essentially variables meant to be exposed to the player as visible information. Every time a quality changes, this is made visible to the player.[2]
Consequently, all changes to persistent state in lyre are explicit; the language disallows silently mutating state as a general rule.
The story is made up of storylets, individual narrative nodes. Every storylet has branches, possible options that can be chosen from them. Branches lead to other storylets; once the player has completed a run of storylets, however, they can then return to a hub area. Lyre calls this the anchor.
Qualities determine which storylets are available and the possible outcomes of branches, but players are in control of which branches they take; the language is designed for quality-based narratives as open story spaces rather than as a way of assembling a story by supplying specifically what happens next.
This scopes lyre down to a practical design. It's not trying to cover every use case, and particularly it's not trying to cover the main use case of Yarn node groups.
A taste of lyre
Here's a commented sample of what lyre source code looks like, which is a partial implementation of Road of Darkness:[3]
// Lyre files are made out of nested, structured definitions. This line
// defines a storylet with `start` as its internal name and "Forest Clearing"
// as its visible title; the `anchor` keyword marks that this is a hub area
// the story returns to if it has nowhere else to go.
storylet anchor start "Forest Clearing"
"""
You stand before a crossroads that splits to the left and right. A man
stands next to the path on the right, wearing a large black velvet cloak.
{
// Arbitrary expressions are allowed in { } blocks inside body text, and
// their evaluation gets interpolated into the text as you'd expect.
if scarf_color then
// You can even nest interpolated text inside other interpolated text.
`Your { scarf_color } scarf is reassuringly warm.`
else
"You are wearing a warm scarf." }
"""
branch "Examine your scarf"
"""
What color was it again?
"""
// `show` creates a condition on whether this branch is visible;
// there's also `req` for locking a branch without hiding it.
// Conditions in lyre require special syntax, because they have to be
// legible to the language system which creates a player-visible
// description. Specifically, they always have to name a quality
// and a comparison operation in a sort of subject-verb-object
// structure.
show ( scarf_color == "" )
-> select_scarf_color
branch "Take the left path"
// Just like in Storynexus, lyre inherently supports "branch text",
// ie additional detail or extra story attached to branches.
"""
It looks well-lit. Inviting even.
"""
// Every branch leads to one or more other storylets. They can have
// arbitrary conditions; the story routes through the first one
// with a condition that evaluates to true.
( asked_about_thief ) -> ending_escape
// The compiler enforces that branches must have a default route.
-> ending_death
branch "Talk to the man"
"""
What's he doing hanging around here?
"""
-> thief_conversation
branch "Examine the cloak"
"""
It drinks in the light, doesn't it?
"""
// A "result" in lyre is like an anonymous storylet that you can
// write right inside the branch. Lyre isn't designed for deep
// tree-like narrative structures; results can't contain branches.
// This strictly limited nesting makes lyre's syntax work, where it's
// neither a bracket language, nor an indentation language, nor a
// depth-marker language like Ink.
-> result "Curious indeed"
"""
It drapes rather loosely about his shoulders... to your trained
eye, it looks rather like a powerful magical artifact that absorbs
the light from the surrounding area.
"""
effects if ( !cloak_knowledge )
+cloak_knowledge
// Every quality has to be explicitly declared. Many of them will have
// information meant to go into an inventory or game state screen.
quality cloak_knowledge: boolean "Examined the Cloak"
"""
You've taken a closer look at the Cloak of Darkness.
"""
// Lyre comes with a system for creating generic messages to communicate
// quality changes (and an API for replacing that with a custom system).
// But a quality can also have a custom function to create those messages;
// this is the simplest case where it's just the same string every time.
describe_change: () => "You add your observations to your mind palace."
Structure, affordances, and influences
Most narrative scripting languages are designed like markup languages; mechanical information exists inside "islands" in a "sea" of body text. Lyre isn't designed like this, mainly because lyre stories are closer to Inform stories than to Ink stories; the ratio of mechanical information (code) to textual information (content) is higher. Text exists as quoted strings, separate from code.
In lyre, state is meant to change only through explicit quality changes. In the sample above, for example:
effects if ( !cloak_knowledge )
+cloak_knowledge
Means "if cloak_knowledge is false, then set it to true."[4] This is only allowed within effects blocks inside storylets (or results, which are basically just anonymous storylets).
Basically, every time the player takes an action in lyre, all of the effects of that action happen sequentially and they all live in the same place. It's a different way of thinking about narrative state—Ink, Yarn, ChoiceScript, and Twine make no distinction between code that can change state, and code that can only reference it.
But lyre's event model, which is obviously heavily influenced by StoryNexus, has a lot of utility for certain types of stories. It makes changes to state easier to see and reason about, which is beneficial for heavily stateful stories. It makes both the compiler and the runtime significantly simpler; a lot of the operations that the lyre runtime does are "pure" in the functional-programming sense, ie they can't mutate state. It enforces the idea that every state change is a visible thing that can be looked at and surfaced to the player.
Lyre is very unabashedly designed around my own preferences about workflows. It's meant to be written in a regular-ass text editor—I actually already wrote a tree-sitter parser for it—with no hard requirement for a specialized tool or IDE. A language server with some basic completion would be nice to have down the line, but it's hardly required. The syntax is meant to make large volumes of prose comfortable to write while also being relatively simple to parse. Lyre's parser is built by ANTLR, the same parser-generator[5] tool used by Yarn Spinner. ANTLR is unusually smart in various ways that lyre makes use of, though some aspects of the syntax are still in flux.[6]
Lyre is obviously heavily influenced by StoryNexus; it's in a lot of ways an attempt at building something more flexible and usable than SN while preserving its specific model of narrative state and player interaction, which I am very fond of. Lyre, in particular, is designed explicitly around the idea that it can work equally well as narrative middleware similar to Yarn, as the core of a full-fat "authoring system" like Inform, or as the narrative engine of a server-based game like StoryNexus.
But it's not quite a "clone" of StoryNexus. It doesn't have any of the SN features that are purely tied to its live-game nature, like living stories or world qualities (though those are something you could very easily implement around the language). It doesn't, and isn't intended to, have a CMS-style author interface. As a language with a compiler, lyre probably doesn't scale to several million words of content straightforwardly.[7]
The language tries to regularize various things about storynexus. In lyre, the player is always inside a storylet; there's no concept of a "location"—the anchor feature is intended to fulfill the role that locations have. Lyre is generally designed around the idea that the only state that exists, that the runtime cares about, is the player's current storylet and their qualities.
But lyre also takes some significant inspiration from Inform, Yarn, Ink, and a whole pack of real programming languages. Lyre's compiler and the first implementation of the runtime are both written in Typescript,[8] and some syntactic conventions (like using backticks for interpolated strings and the "fat arrow" to signify functions) seep in from that. Lyre currently has only one collection type, which are sets—largely inspired by Ink's lists but named something other than lists because lists, in Ink, are not meaningfully lists in any normal sense (why are they called lists?).
But lyre is also weirdly influenced by pure functional languages, particularly Haskell. The fact that Lyre puts stateful operations (ie, anything that creates a quality change) in a little box where they can't hurt anyone is very analogous to Haskell's treatment of IO. Quality effects are not currently implemented as a monad, but they conceptually are one.
Implementation-wise, lyre superficially resembles Ink and Yarn in that lyre stories compile down to a JSON bundle that the runtime uses. But lyre doesn't have the same kind of "engine"; it doesn't have anything inside it that really resembles a virtual machine. Most of the lyre runtime is a narrative engine dedicated to lyre's specific semantics around branching, determining what the player sees, and so on. Lyre does compile and evaluate arbitrary expressions, but for the sake of compiler and runtime simplicity those are compiled down just to s-expressions in a specified JSON-compatible format. The runtime is more like an old-school lisp reader or scripting language interpreter.
This is not efficient, but it's extremely easy to implement—lyre's expression evaluator is only about 100 lines of typescript code, not including the definitions of the language's builtin function primitives. I think making it as dead-easy as possible to port the runtime to other environments is a very significant goal for something like lyre; the reality of game dev right now is that you generally can't run code written in any arbitrary language in any given game, because most engines really don't want you to. And while there's a few things about the runtime that I want to revise, refactor, and ideally simplify even more... it is also true that I ported the lyre runtime to GDScript already and it wasn't all that painful to get it working and passing the same test suite the reference implementation passes.
Some More Examples
These come from the project I'm currently working on with lyre, and show off
some other language features.
// Functions in lyre are always pure; they're a way of reusing chunks of
// expressions rather than a way of "doing" things.
fn desc_movement (f, t, n, v) => (
if f && f == location then `{ n } { v } away to the { loc_name(t) }.`
else if t == location then `{ n } { v } in from the { loc_name(f) }.`
else "")
quality harriett_location : string "Harriet's Location"
describe_change: (f, t) => desc_movement(f, t, "Harriett", "glides")
quality dean_location : string "The Dean's Location"
describe_change: (f, t) => desc_movement(f, t, "The Dean", "squirms")
/* ... */
// A `table` in lyre is just a way of writing a bunch of variable text; these
// are a generalization of the main variable-text feature in StoryNexus.
table dean_next_location
with dean_location
== "living_room": "dining_room"
== "dining_room": "downstairs_landing"
default: "living_room"
// Like functions are reusable chunks of expressions, procedures are reusable
// quality effects. Like Ruby and some other languages, lyre adopts the
// convention that effectful actions have a ! on them; this is actually
// enforced (mostly because it greatly simplifies parsing).
proc move_dean! ()
dean_location = dean_next_location()
/* ... */
storylet anchor kitchen "Icebox Temple"
"""
The kitchen is small and modest relative to the rest of the house.
Notably, you see not one actual cooking implement other than a well-loved
coffee percolator.
"""
{ room } // This is a tag, marking this storylet as a room.
{ location = "kitchen" } // This is an alternate syntax for a single effect
// This is an arbitrary key/value pair attached to this storylet, which
// the surrounding game can use to decide how to display the storylet.
// For example, this is how you'd implement giving a storylet an
// illustration.
loc_name: "Kitchen"
branch "Dining Room"
"""
Back to where the bar is.
"""
direction: "n"
-> dining_room
/* ... */
storylet conversation_dean "Dean Liam Harcourt, Holding Court"
"""
( actual text excised to avoid spoilers )
"""
// This is how lyre actually implements stories that can grow organically
// and "true" storylet narrative. A `from` block creates a branch, but
// the branch leads to the surrounding storylet, and it has a reference
// to where it's available. In this case, it's available from every
// storylet that has the `room` tag, so the dean can wander around and
// you can talk to him wherever he is.
from { room } "Talk to Dean Harcourt"
"""
( snip )
"""
show ( location == dean_location )
Lyre will be available to people who are not-me... eventually. Look forward to the currently-untitled game at the end of October.
StoryNexus has a few other chunks of game state that aren't qualities, like the player's current location or their lodgings. In lyre, everything truly is a quality. ↩︎
Both StoryNexus and lyre have a concept of "hidden" qualities that aren't surfaced to the player. But the idea of exposed narrative state is central to StoryNexus' design. ↩︎
Road of Darkness is a Cloak of Darkness analogue meant specifically for choice-based narrative systems (while Cloak of Darkness was envisioned as a way of exploring how different parser-game systems express world model). In truth neither is a particularly good fit for lyre; maybe I should write a spec for Island of Darkness?↩︎
The condition is in fact not redundant here. Lyre, like StoryNexus, will generate quality change events that say "Such-and-such quality remains unchanged" if you tell it to. Ie, setting a value that's already true to true again isn't a no-op. ↩︎
A parser generator is a tool that takes a grammar (a formal specification of the rules that define a language) and generates the source code to an implementation of a parser (a computer program that recognizes and understands a language). These are not AI in any sense, and they've been around for around 40 years. ↩︎
I have a somewhat dumb idea of giving lyre Haskell-like expression syntax, ie a style of writing expressions that makes parentheses largely unnecessary. I'll probably not do it because it would make parsing more annoying to please no one but myself, but I'm tempted. ↩︎
This is mostly due to compilation times, which would become unreasonable at that scale, though it may be possible down the line to support partial recompilation and multiple files to ease that. Lyre's runtime also assumes that you can just hold the entire story as an immutable object in memory, though this isn't really such a problem. ↩︎
The reasoning for this choice is that Typescript is, of all the languages that ANTLR generates code for, both the one I know best and the one I hate the least. Obviously having a runtime that can run in a browser is also a very nice thing. The other sensible option would have been C#, but that would have given me a runtime that runs nowhere I care about. ↩︎