Soul Reviews

Story From North America

Monastic Inversion DevLog #1 🏰

Last week1 I mentioned that I'd be participating in the Acerola Jam 02 and things have gone swimmingly.

img

Thoughts On The Theme: Aberration πŸ‘»

A departure from what is normal, usual, or expected, typically one that is unwelcome.

Immediate thoughts about specters and ghosts come to mind, as well as things that could be described as "cursed" like un-intuitive UI elements, statues in the middle of a jungle without any humans for miles, and various strange trees formations.

Nah, truth is, I knew what I wanted to do from going in. I wanted to make a Shin Megami Tensei-like RPG. Which was perfect for this theme with all of it's super natural elements, twists in settings, and just demon this and that, but it also got me thinking: what's more aberrant then erratic spell-casting? and suddenly the theme hit me.

img

Combine the erratic nature of Magicka's spells, with the dice-drafting mechanics of something like Sagrada, and finally toss in some story and visual inspiration from SMT IV, and we've got ourselves a high minded ambition! Worthy of two weeks of work!

But can you do it? 🀨

My choice is from a position of experience, after all, I've done this before, well, MOSTLY I mean3 :

img

Last year, during Lent I made You Must Play, a deck-building card game where actions are sequenced by the player and the ordering matters. It was a huge learning experience where I found out things like:

Turns out making a card game is difficult, but having climbed that mountain once, I'm sure I can do it again, and better this time around. Also, Acerola has given a pretty unusual submission period of the jam, at least from my experience, of two weeks. This was in order to give people with busy schedules time to submit something, I however will be exploiting this to it's fullest extent in order to make a nice, sophisticated system.

Current Progress πŸ“Š

Right now what exists is the battle system, a walkable 3D environment, some animations, and a lot of ideas that need implementing. Not a great place to be halfway through a jam? Think not! Allow me to explain.

The Battle Life-cycle

img

This is an abstract view of what currently exists for the battle system.

In retrospect, it was much harder to come up with this then explain it after the fact, but I believe I've addressed all the core concerns raised in You Must Play:

But How About That Data? πŸ“‰

Nothing in life is free. Especially composable, extensible, easy-to-work with systems. Thankfully, it's all just data and data is easy if you take a second to sit down and think about it.5

img

For the most part I hope this is self-evident if you understood how the BattleSystem is written above, but some fun key points should stand out:

But what does all this complication buy me? πŸ’Έ

Flexibility, re-use, and rapid iteration:

img

This is everything I need to do in order to implement a simple enemy attack. If I wanted to, I could also have the enemy deal damage back to himself after dealing damage to the player, or propose whatever other change, with whatever targets he wants and all I have to do is specify in the attack definition within whatever parameters I need. It's pretty sweet and I'm looking forward to seeing what combinations I can create. TriggeredEffects and StatusEffects follow similar patterns.

img

As stated previously, a TriggeredEffect is just an Effect that has some trigger, a condition. The target for the effect is decided by the TriggeredEffect. I'll probably refactor it to be a list of effects instead for greater flexibility, but this is fine for now. If the trigger isn't activated, apply_triggered_effect just returns an empty list. Also notice, I've re-used the DealDamage effect! Already removing redundancy!

img

StatusEffects are more like MutateEffect since they don't produce new LedgerEntries like TriggeredEffect they just mutate whatever LedgerEntry was passed to it and call it a day. In that sense, they're not true Effects and don't extend the Effect class. And yes, if you stack the same status effect on an enemy over and over again it will apply each time:

img

Some future things I'm thinking about implementing:

The Cost πŸ’΄

Okay, you got me, all of this isn't free even if I have grand plans that have worked out. Having Effects create Changes and LedgerEntries means that I still have to use those changes to, you know, change the state?

# Matching the LedgerEntry type
Enums.EntryType.EFFECT_APPLIED:
    # Matching the change effect in the LedgerEntry
        match change_entry.effect_type:
                Enums.EffectType.DAMAGE:
                    # Writing the change to battleStats
                        for stat : Stats in change_entry.effect_target:
                                stat.health -= change_entry.effect_value

# but wait, there's more!
# Add the entry to the ledger
ledger.append(change_entry)
# Tell everyone about the new change
EventBus.new_ledger_entry.emit(change_entry)
# Play the change
# Animation can't just wait for the `new_ledger_entry` signal because we have to
# Wait for the animation player to finish
await animation_player.play_ledger_entry(change_entry)
# Take care of any Stats that died and should no longer be considered
# Also, we emit death LedgerEntries here that may also need to be played
await cleanup_dead_targets(battle_stats,targets,caster,ledger,animation_player)

Yup, the dirty part of all this is that there exists an abstraction below Effect called EffectType that actually determines the mutation applied to the BattleStats. In some sense, a good 90% of the game logic is going to go in one big switch statement. But before you think this is the worst thing ever, consider how many different types of state changes actually occur in card games?

All things considered, once you've reasoned your way through how the life cycle of how cards operate with applying/mutating/triggering effects, the actual changes that take place to state mostly boil down to changing health values, shuffling things(quite literally sometimes) around, and ticking some clocks up and down. So, I really don't mind having all this stuff in one switch. Frankly, I think it's simpler this way.

Now animations are a bit of a different story. Each LedgerEntry has to be played in some way so the player can see things exploding and how their decisions are affecting the world, so for every case in that switch, we also need a case in the animation player with an accompanying animation.

    func death_animation(enemy_repr: Node2D) -> Signal:
            """Play the death animation for an enemy"""
            var tween : Tween = create_tween().set_trans(Tween.TRANS_CUBIC)
        # Just fade the enemy into nothingness
            tween.tween_property(enemy_repr,"modulate",Color(0.0,0.0,0.0,0.0),0.5)
        # Pass the 'finished' value back so the caller knows when the animation is done.
            return tween.finished

Now the truth is, I can do a fair bit of massive cheating here by just using programmatic animations over a single enemy texture and then it doesn't matter what enemy it is, I just automatically have all the animations I created thus far.6 You can also see that the enemy_repr is there, passed from the LedgerEntry, so no weird reach around had to occur for the AnimationPlayer to do it's job, it just HAS that data from the log entry.

The one annoying limitation is that animations are always played after an effect is applied and then waited on so I can't, for example, combine a lightning strike with a death animation to create a struck by lightning so hard the enemies skeleton shows, then they crumble to ash, dead animation because the DAMAGE and the DEATH entries are two separate logs! I'm considering potential fixes for this, such as having buffering logic for certain animations like all attacks and critical damage, but I believe I need to start making some content because I have until Wednesday evening to make something complete with these systems and this is where I draw the line.

The World 🌲

My plan is for this jam is to have a simple 3D environment the player explores, picks of shiny rocks, finds points of interest, and encounters enemies in.

img

My experience with 3D is slightly above beginner at this point. I have a Blender layer for my ergo keyboard and I don't really struggle to make shapes anymore, but that doesn't mean I can design good spaces, in my mind, that takes a lot of continuous iteration and time I don't have. I've seen what alleged "hobbyist" modders can do with GZdoom, let alone the Counter Strike 2 brush editor, I can't compete with that at the moment7. My hack is going to be fog, akward textures, low light, and a future screen-space shader8 that'll hopefully push the aesthetic bar high enough to be creepy.

For movement, in order to pay homage to previous SMT games9, I've restricted it to the four cardinal directions, plus strafing left and right, this means environments have to be designed on an explicit grid, 2x2 meters here. I don't support stairs at the moment and don't think I'll bother, rather just teleport the player after some footstep sfx.

What This Next Week Look Like 🫠

It's gonna be a grind until Wednesday baby. My do outs are:

I have a play test Saturday morning, so I'm compelling myself to finish a playable demo by then and plan to spend roughly eight hours a day on this. Time to ball.

Footnotes