Well that was an adventure. I just coded a Hearthstone-style rules system in a few days, only taking d8 psychic damage in the process.
But I think it's pretty cool. Here's the basic "entry" for the rules logic for a given action:
public void Wait(Action onComplete = null) { Depot.Generated.dungeon.logicTriggers.wait.Emit(Systems.Logic.RootEvent, postExecution: e => { Dungeon.Track.Act(e); }, onComplete:onComplete); }
This is doing a few things. First is that that long string of Depot path is the type-safe data emitted from my Depot integration that allows me to reference whatever I put in this, directly:
On top of that I then wrote extension methods that actually "Emit" the event into the action graph (again channeling this blog). I had a stroke of genuis for this bit that I think is worth a proper standalone blog about, and maybe as its own follow up to that blog I'm referencing, but basically what I do is bind the keywords themselves to a static dictionary that maps to "loose" static IEnumerator functions.
"Adding an event to the graph" then basically creates and underlying Event object that saves the references IEnumerator, and then invokes it when it's the events turn. The other thing I did on top of this was tie in sensible event lifecycle callbacks to modify the action graph as it runs, primarily through OnExecuted and OnComplete.
OnExecuted runs when an action graph node has been executed... which is a perfect time to add side-effect nodes as children of that node! The event polling system finds the new children, and executes as normal. What's also cool is that when you use the Emit function, it scans for matching keywords from cards such that it will also then tack on the "Will Do X" events to a node before execution, ensuring they update first.
OnComplete is called when a node is FULLY complete, as it has no more un-executed children. Tying into here can basically surface top-level callbacks for actions that may have had hundreds of reactions, etc. Here's a more robust example of how to trigger the "basics", and keeping in mind that under the hood all the graph event stuff is being automatically generated!
public void Move(Action onComplete = null) { Depot.Generated.dungeon.logicTriggers.move.Emit(Systems.Logic.RootEvent, postExecution: e => { Depot.Generated.dungeon.logicTriggers.discard.Emit(e,new Systems.Logic.EventData(Dungeon.Track.Cards[0].ID), postExecution:e => { Dungeon.Track.MoveTrackCardsToLatestTrackPositions(); Depot.Generated.dungeon.logicTriggers.draw.Emit(e,onComplete: () => { Dungeon.Track.MoveTrackCardsToLatestTrackPositions(); }); }); }, onComplete:onComplete); }
The idea is that you have a unified entry point for all the main actions in the game that have pre-determined steps that execute, but at the same time any cards with interesting keywords/effects can automatically tie into this to build out a more robust graph of effects!
What's ALSO cool is that, to debug stuff, I tied in a simple way for the graph to report itself using Mermaid, so I can basically (visually) peek at the internal graph at any time. I'll set a breakpoint in the middle of that function above, and here's what the graph spits out:
flowchart TD 0[_0]-->7[move_7] 7[move_7]-->9[discard_9] 9[discard_9]-->10[draw_10] 0[_0]-->8[DeathReap_8]
Which visually resolves to:
This is a pretty simple state to visualize but it's cool to see it all working!
With this in place a ton of things are now possible so I'm hoping to actually crank on content between now + deadline and get some real gameplay in. Oh and I guess and inventory... and some spells.... we'll see!