Hi there! Figured I’d break the ice on this topic and make a post about my intended jam project.
I knew I wanted to do something based on an existing or upcoming TTRPG I made, and when I looked at my existing and upcoming projects, one of them in particular stood out: Machinations of Court and Frame. So I’m gonna make a little story in that universe. My goal is to write a little VN-driven story about a planet caught up in the intrigues of three Houses, and all the people representing those Houses clashing.
My general outline is:
Intro: Lifepath-style character creation: three starting scenarios push you through some combination of four secondary scenarios. These introduce you to some of the cast - relevant because your relationships to the cast are your “skills”.
Act 1: The taking of a planet, formerly independent, due to uprisings providing an excuse to do it. Everyone in the cast is working together on this, so you’ll meet everyone else.
Act 2: Dealing with power-sharing between the three Houses, unrest, uprisings. Make some loaded decisions about how to split resources. Build tension.
Act 3: One of the two Houses you don’t work for tries to take it all. Everything goes to hell.
This is pretty ambitious (especially since I want choices to propagate forward a lot) so my goal for this jam is to finish at least the intro (which is like half written and mostly coded, no styling yet) and act 1 to create a vertical slice.
Features planned:
As mentioned, character relationships are skills. In general this setting isn’t really concerned about like, your agility or strength or whatever so much as who you know, how well you know them, and any specific decisions made. In general these will be fairly un-random: they go from 0 to 6 and add to a roll of 0-2, so once you have 4 you’ll be able to hit every Difficulty 4 check 100% of the time, etc. Sometimes it’ll check your starting background.
There’s no standard fantasy turn-based combat. But there are mecha duels! I’ve outlined a proposal for how this works in Total//Effect but basically it’s kind of a rock-paper-scissors-ish thing with some funky stuff with the normal roll engine (with other stuff involved, but I’m leaning more into the RPS aspects given the engine). (This is largely coded at the moment, just need to make sure that a few things work the way I want them to.)
I’ll post updates here when I have 'em. For example, I’m gonna make a chost at some point about TTRPG adaptation: what I’m changing from my design docs for the design of this and why.
Success! (It’s ugly because I haven’t done much CSS on it, but it’s functional. And obviously, this is a canned scenario as opposed to a proper fight.)
Start of the round: armor decay, evasion decay, remove conditions.
Your opponent picks a move randomly. If you have a Tell on that move (in game, these are often gained from Relationships with that person or from help granted via one), you know what it is. If not, you just have to guess.
You pick as well. (They re-roll Defensive ones once if they have more than half health and re-roll non-defensive if they have less, so you’ve got some idea based on how the fight’s going.)
Both actions always happen. If someone wins a RPS-matchup, they get a little bonus and their action goes off first. Right now ties go to the player.
I’ll do a teardown on how it works under the hood once I’ve finalized the normal procedural setup. It’s got some funky stuff going on but it’s a pretty solid pattern for the most part once I got over the lack of return/pass-by-reference.
moves.yaml, which describes those moves and provides some metadata:
headshot:
name: Headshot
type: Aggressive
subpriority: 2
description: "Aim for the cockpit."
brace:
name: Brace
type: Defensive
subpriority: 2
description: "Direct more armored parts of your Frame towards the enemy."
bodyshot:
name: Bodyshot
type: Indirect
subpriority: 2
description: "Aim for an unarmored part of their body."
And tells.yaml, which describes specific Tells that can be set prior to combat situationally:
duel_test_headshot:
move: "headshot"
description: "Debug tell description for Headshot."
The general logic goes something like:
Set up the player Frame (usually only done once at the start of the game, I’ll probably make a secondary refresh loop to do things like reset Integrity to max)
Set up a specified enemy Frame for the scenario (uses the same mechanics, so just a different Frame from the list)
Set any Tells (called out by name, though I’ll probably create a function to do it procedurally at some point too for the main characters)
Start the main duel loop
The main loop is:
start_duel_round win_label loss_label:
run enemy_picks_move
run check_for_tell
run start_of_round_refresh
run set_player_moves
if (<= $player_frame.integrity 0):
jump $loss_label
elseif (<= $enemy_frame.integrity 0):
jump $win_label
else:
jump start_duel_round $win_label $loss_label
Which is to say:
Enemy picks its move. This is done by picking from its list of moves twice via random_from_array:
If it’s got more than half health and the first move is Defensive, it’ll pick the second.
Vice versa if it’s got less than half health: if the first move isn’t Defensive, it’ll pick the second.
The second doesn’t get re-picked though, so first-half moves can still be Defensive and second-half moves can still be non-Defensive.
That move is checked against the list of Tells set for the scenario. If it matches one, the first time it’ll spit out that description (“Debug tell description for Headshot.”, in the example above, and then tell you explicitly that the opponent is going to use that move. Any time after that, it just tells you the second part.
The redundancy check took surprisingly long to figure out how to do compared to the rest of this.
Start of round refresh. For both Frames, subtract 1 from any Armor, add 3 to any Evasion, zero any negative or > 6 Evasion, clear any conditions. This is also where things like Conditions can sometimes kick in and provide exceptions (for example, the Evasive condition suppresses the “add 3 to Evasion” step).
Generate move choices for the player. This is where programmer brain got real sad because I couldn’t do this in a more data-oriented manner. I make a choice for every move with the condition that it only shows up if the player has that move:
choice:
"What will you do?"
"Headshot (Aggressive)" if (includes $player_frame.moves "headshot")
run clash "headshot" $enemy_frame.current_move_name
"Brace (Defensive)" if (includes $player_frame.moves "brace")
run clash "brace" $enemy_frame.current_move_name
"Bodyshot (Indirect)" if (includes $player_frame.moves "bodyshot")
run clash "bodyshot" $enemy_frame.current_move_name
The player picks a choice, then that clash function you see above happens:
First, the game grabs the two moves from our moves.yaml-loaded object based on the parameters passed.
It then grabs the priority and subpriority. The first one is our RPS: Aggressive > Indirect > Defensive > Aggressive. The second one is for ties within draws in priority: Higher means that action happens first. (Buff/purely-defensive moves > long-ranged moves > short-ranged moves > melee moves.) If there’s a match in priority, tie goes to the player.
Based on that order, the move with priority/subpriority goes off first, followed by a check for if the 2nd actor is dead/Disrupted (condition that cancels the 2nd move), followed by the second move. The move is passed priority as -1, 0, or 1, as well as the initiator. (Sometimes I don’t use -1, but most of them do something extra on 1.)
Here’s what a move looks like! This always sets its target’s Armor to 0, then deals 4 Harm at high Priority, or 2 at low Priority. (The harm function checks if the target has Evasion <= the Harm and negates it if so, then subtracts any armor from the Harm before subtracting it from Integrity.
bodyshot priority initiator:
var target "enemy_frame"
if (== $initiator "enemy_frame"):
think combat_enemy idle "Your opponent takes aim at your weak points."
set target "player_frame"
else:
think combat_player idle "You take aim at your opponent's weak points."
run set_armor $target 0
if (== $priority 1):
run harm $target 4
else:
run harm $target 2
I’m glad to see that the feature to import yaml files is getting some use. So far (outside of myself) people mostly made simple games that didn’t use much gamey logic.
But it is really nice to be able to create arbitrary data files to generate more dynamic content, I’ve found.
Yeah, at the end of the day I’m way more of a game designer and programmer than a (prose) writer or visual artist - so I’m inevitably going to use those kinds of features a lot more, haha.
End of week 2. For real-life-related reasons (and due to announcing a TTRPG!) I didn’t put as much time towards this. But what I did accomplish was:
Statted out 3 more Frames for the scenario.
Diagrammed out most of what it looks like.
Finalized names for the main characters that matter (you’ll meet them all over time but every character meets 4/8 of them in their lifepath-intro to start):
At pilot academy (Knight/Scion), you’ll meet:
Caterina Alzur (she/her), Heir to House Alzur. Cold, obsessed with not letting her family down. Naive.
Gustav Weber (he/him), Heir to House Weber (not a Great House like the others, just a local one on Amirus). Warm but a little flaky, always angling for something.
At finishing school (Scion), you’ll meet:
Alex Reyaal (they/them), Heir to House Reyaal. Cordial but a bit surface-level, fairly reserved.
Alyssa Montrant (she/her), Heir to House Montrant. Exuberant. Haughty but with a good sense of humor. Quick to anger.
If you came up in the Arena (Captain), you’ll meet:
Joe (they/them), hotshot pilot hired by House Montrant. Brash, self confident. A little insecure when questioned.
Paula (she/her), seasoned tactician hired by to House Reyaal. Extremely analytical, not a people person.
And if you got deployed at Nilara (Knight/Captain), you’ll meet:
Kenji (he/they), Knight of House Montrant. Very forthright and formal. Long-suffering guardian of Alyssa.
Theo (he/him), Captain of House Reyaal. Extremely friendly. Not amazing at command, but enough of a people person that it mostly works out anyway.
Wrote the two big dialogue “loops” in the intro:
First, an expository one: as you land, Caterina will engage you about various things, some of which change a little depending on your background. Mostly this sets the stage for what you’re doing, which is retaking the planet for the local nobility.
Next, after landing: you’re reunited and get to have a little chat with anyone you know.
And wrote the big act 1 fork: based on whether you know Montrant or Reyaal folks better, you’ll be sent to either take out a communications array or secure a noble estate prior to the assault on the capital.
We have an official title! Unless I change my mind.
Comms array fork is written. You’ve got a choice between avoiding combat and taking a short-term good, long-term bad result, or accepting combat and risking a short-term bad result for a long-term better result. I think I’ll do something similar with the other path too.
The way I’m doing combat, failing (either getting put to 0 Integrity or taking too long to win) isn’t a “start the game over”, it’s just one branch. In this case, it opens up a new dialogue option in the aftermath that you don’t get on either of the other results.
And as it’s not terribly visually interesting right now and I have no particular interest in seeking out backgrounds during a jam, I licensed some lovely portraits for commercial use that I’d been eyeing for a few other projects anyway.
Draft is up! (Still restricted for now, password is embedded in that link.)
I’m fine-tuning a few things (like for instance, I’m probably going to reduce the portrait size so loading is less of a thing) but it should be up for real and submitted sometime today!
Thanks! Some bits are still a little buggy, it’s very easy to drop a jump or run here and there on one branch of an if statement, but I think I’ve gotten rid of all the grievous engine-explosions.