Spellweaver's Talent Kitchen

This bundle is marked as pending. It has not been reviewed by a staff member yet.
A vJass system made for implementing talent systems into a map. It provides a way to build a talent tree from an easy to use API and link game logic to it. It also provides a user interface and handles the changes automatically.

Some features it has...
  • Single rank and multi rank talents. Each rank is defined separately and can have its own text/icon/behavior
  • Logic binding on Activation (when talent effects are applied), Allocation (when talent is taken before confirming), Requirements (custom disable logic)
  • Talent point tracking, each talent/rank can have a varying talent point cost (0, 1 or more)
  • Requirements (custom logic based) talent disable, the user defined text reason will be shown in the tooltip
  • Talent Dependencies, in cardinal direction, talent can require its adjacent talents to have a specified level, this is shown in interface as "links" or "lines"
  • Talent-tree based configurable rows and columns, for example Pyromancer can have 6 rows, 3 columns and Fighter can have 4x4
  • Grid based talent positioning (X, Y) and automatic button placement based on rows/columns
  • Temporary talent point allocation, points can be put into tree and reset as many times as needed before "Confirm" is clicked, only then all the effects will apply

It has one huge drawback at the moment though. There's no out-of-the-box save/loading and would have to be written custom until I add such functionality in the API.
Another is that documentation is not extensive nor structured. Example should provide more than enough information for implementation/trying things out for now though.
It's also fresh out of the kitchen, so there might be some bugs I haven't been able to find and iron out.

18.11.2021 - Posted
21.11.2021 - Multi-panel trees, Link-talents, More accessible customizability, RUID integration, github repo link + DruidClassic example
22.11.2021 - Added Relative version of the Druid map UI. Resizing or moving the frames will affect the rest of the frames.
TODO - Documentation for Talent API
TODO - Documentation for custom views (RUID integration?)
TODO - Save Load API

Instead of pasting code here (since it lags a lot) here's a link to a Github Repo. Code from all current and future examples will be there as well.


Tags: Talent system, talent tree, ui

vJASS:
scope Shepherd initializer init

    public struct Shepherd extends STKTalentTree_TalentTree

        method Initialize takes nothing returns nothing
            local STKTalent_Talent t

            call this.SetColumnsRows(5, 6)
            set this.title = "Shepherd"
            set this.pointsAvailable = 6
            // set this.icon = "FireBolt"
            // TODO: set tree background texture here

            // The tree should be built with talents here
            // ==============================================

            // Wondrous Flute <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Rank 1
            set t = this.CreateTalent()
            call t.SetName("Wondrous Flute")
            call t.SetDescription("Calls a sheep to your side.")
            call t.SetIcon("AlleriaFlute")
            call t.SetOnActivate(function thistype.Activate_CallSheep)
            call t.SetCost(0) // First level of this talent is free
            call this.AddTalent(0, 0, t)

            // Rank 2
            set t = this.CreateTalent()
            call t.SetName("Wondrous Flute")
            call t.SetDescription("Calls another sheep to your side.")
            call t.SetIcon("AlleriaFlute")
            call t.SetOnActivate(function thistype.Activate_CallSheep)
            call this.AddTalent(0, 0, t)
            // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Soothing Song <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Rank 1
            set t = this.CreateTalent()
            call t.SetName("Soothing Song")
            call t.SetDescription("Calls a flying sheep to your side.")
            call t.SetIcon("DispelMagic")
            call t.SetOnActivate(function thistype.Activate_CallFlyingSheep)
            call t.SetDependencies(1, 0, 0, 0) // left 1 (left up right down)
            call this.AddTalent(1, 0, t)

            // Rank 2
            set t = this.CreateTalent()
            call t.SetName("Soothing Song")
            call t.SetDescription("Calls another flying sheep to your side.")
            call t.SetIcon("DispelMagic")
            call t.SetOnActivate(function thistype.Activate_CallFlyingSheep)
            call t.SetDependencies(1, 0, 0, 0) // left 1 (left up right down)
            call this.AddTalent(1, 0, t)
            // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Shepherd Apprentice <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Rank 1
            set t = this.CreateTalent()
            call t.SetName("Shepherd Apprentice")
            call t.SetDescription("Gain an apprentice.")
            call t.SetIcon("Peasant")
            call t.SetOnActivate(function thistype.Activate_GainApprentice)
            call this.AddTalent(2, 0, t)
            // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Lots of Apprentices <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Rank 1
            set t = this.CreateTalent()
            call t.SetName("Lots of Apprentices")
            call t.SetDescription("Gain 2 guard apprentices.")
            call t.SetIcon("Footman")
            call t.SetOnActivate(function thistype.Activate_Gain2Guards)
            call t.SetDependencies(0, 0, 0, 1) // down 1 (left up right down)
            call this.AddTalent(2, 1, t)

            // Rank 2
            set t = this.CreateTalent()
            call t.SetName("Lots of Apprentices")
            call t.SetDescription("Gain 2 guard apprentices.")
            call t.SetIcon("Footman")
            call t.SetOnActivate(function thistype.Activate_Gain2Guards)
            call t.SetDependencies(0, 0, 0, 1) // down 1 (left up right down)
            call this.AddTalent(2, 1, t)
            // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Coming of the Lambs <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Rank 1
            set t = this.CreateTalent()
            call t.SetName("Coming of the Lambs")
            call t.SetDescription("Gain a Sheep and a Flying Sheep.")
            call t.SetIcon("Sheep")
            call t.SetOnActivate(function thistype.Activate_ComingOfTheLambs)
            call t.SetDependencies(0, 0, 1, 1) // right 1 down 1 (left up right down)
            call this.AddTalent(1, 1, t)

            // Rank 2
            set t = this.CreateTalent()
            call t.SetName("Coming of the Lambs")
            call t.SetDescription("Gain 2 Sheep and 2 Flying Sheep.")
            call t.SetIcon("Sheep")
            call t.SetOnActivate(function thistype.Activate_ComingOfTheLambs)
            call t.SetDependencies(0, 0, 1, 1) // right 1 down 1 (left up right down)
            call this.AddTalent(1, 1, t)

            // Rank 3
            set t = this.CreateTalent()
            call t.SetName("Coming of the Lambs")
            call t.SetDescription("Gain 3 Sheep and 3 Flying Sheep.")
            call t.SetIcon("Sheep")
            call t.SetOnActivate(function thistype.Activate_ComingOfTheLambs)
            call t.SetDependencies(0, 0, 1, 1) // right 1 down 1 (left up right down)
            call this.AddTalent(1, 1, t)
            // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Call of the Wilds <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Rank 1
            set t = this.CreateTalent()
            call t.SetName("Call of the Wilds")
            call t.SetDescription("Five hostile wolves appear.")
            call t.SetIcon("TimberWolf")
            call t.SetOnActivate(function thistype.Activate_CallOfTheWilds)
            call t.SetRequirements(function thistype.Requirement_CallOfTheWilds)
            call this.AddTalent(1, 2, t)
            // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

            // Only need to call this if some talents start with certain rank
            // call this.SaveTalentRankState()
        endmethod


        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // Can use these methods inside Activate/Deactivate/Allocate/Deallocate/Requirements functions
 
        // Returns unit that owns the talent tree
        // static method GetEventUnit takes nothing returns unit
        // thistype.GetEventUnit()

        // Returns talent object that is being resolved
        // static method GetEventTalent takes nothing returns STKTalent_Talent
        // thistype.GetEventTalent()

        // Returns rank of the talent that is being activated
        // static method GetEventRank takes nothing returns integer
        // thistype.GetEventRank()

        // Returns "this"
        // static method GetEventTalentTree takes nothing returns TalentTree
        // thistype.GetEventTalentTree()

        // Needs to be called within Requirements function to disable the talent
        // static method SetTalentRequirementsResult takes string requirements returns nothing
        // thistype.SetTalentRequirementsResult("8 litres of milk")

        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

        static method Activate_CallSheep takes nothing returns nothing
            local unit u = thistype.GetEventUnit()
            call CreateUnit(GetOwningPlayer(u), 'nshe', GetUnitX(u), GetUnitY(u), GetRandomDirectionDeg())
        endmethod

        static method Activate_CallFlyingSheep takes nothing returns nothing
            local unit u = thistype.GetEventUnit()
            call CreateUnit(GetOwningPlayer(u), 'nshf', GetUnitX(u), GetUnitY(u), GetRandomDirectionDeg())
        endmethod

        static method Activate_GainApprentice takes nothing returns nothing
            local unit u = thistype.GetEventUnit()
            call CreateUnit(GetOwningPlayer(u), 'hpea', GetUnitX(u), GetUnitY(u), GetRandomDirectionDeg())
        endmethod

        static method Activate_Gain2Guards takes nothing returns nothing
            local unit u = thistype.GetEventUnit()
            call CreateUnit(GetOwningPlayer(u), 'hfoo', GetUnitX(u), GetUnitY(u), GetRandomDirectionDeg())
            call CreateUnit(GetOwningPlayer(u), 'hfoo', GetUnitX(u), GetUnitY(u), GetRandomDirectionDeg())
        endmethod

        static method Activate_ComingOfTheLambs takes nothing returns nothing
            local unit u = thistype.GetEventUnit()
            local integer i = thistype.GetEventRank()
            loop
                exitwhen i <= 0
                call CreateUnit(GetOwningPlayer(u), 'nshe', GetUnitX(u), GetUnitY(u), GetRandomDirectionDeg())
                call CreateUnit(GetOwningPlayer(u), 'nshf', GetUnitX(u), GetUnitY(u), GetRandomDirectionDeg())
                set i = i - 1
            endloop
        endmethod

        static method Activate_CallOfTheWilds takes nothing returns nothing
            local unit u = thistype.GetEventUnit()
            local integer i = 0
            loop
                exitwhen i > 5
                call CreateUnit(Player(PLAYER_NEUTRAL_AGGRESSIVE), 'nwlt', GetUnitX(u), GetUnitY(u), GetRandomDirectionDeg())
                set i = i + 1
            endloop
        endmethod

        static method IsEnumUnitSheepFilter takes nothing returns boolean
            return GetUnitTypeId(GetFilterUnit()) == 'nshe' or GetUnitTypeId(GetFilterUnit()) == 'nshf'
        endmethod

        static method Requirement_CallOfTheWilds takes nothing returns nothing
            local unit u = thistype.GetEventUnit()
            local group g = CreateGroup()
            call GroupEnumUnitsInRange(g, GetUnitX(u), GetUnitY(u), 5000, Filter(function thistype.IsEnumUnitSheepFilter))
            if (CountUnitsInGroup(g) < 8) then
                call thistype.SetTalentRequirementsResult("8 nearby sheep")
            endif
        endmethod
    endstruct

    private function init takes nothing returns nothing
    endfunction

endscope
Previews
Contents

Spellweaver's Talent Kitchen (Map)

STK Druid Talents (Map)

SpasMaster

Hosted Project: SC
Level 22
Joined
Jan 29, 2010
Messages
1,949
At first glance, this looks really cool and really customizeable. Great job.

I have a question.
If I understand correctly (after looking around the code for a bit), when a Talent has more than one dependency (like your "Coming of the Lambs"), currently you need to have both fulfilled in order to unlock it.

Is it possible to have 2 paths leading to a talent, but only needing one of the two dependencies to unlock it?

If not, is it something you can see being added to the system?

Thanks!
 
It's great that you've noticed it, I appreciate you going through my code!

If not, is it something you can see being added to the system?
To answer your question, I haven't had this use case so I haven't implemented it. The way I would go about it would be a DependencyMode on the talent or something that would change the way CheckDependency logic works for that talent.

Specifically, the information is a number (required level) so I don't have much space to navigate in the baseline implementation. I've also had in plan to reserve the -1, -2 etc as flags. One usecase I've had was that I needed a disabled link no matter what, so I would set it to -1 (and handle it appropriately), then later programmatically change it to 0 if I want to unlock it (from perspective of dependencies no matter what).

So yeah it shouldn't be hard to add. Though like I said, it's a bit harder to design a user defined, out of the box boolean expression between the links lol
So saying two links need to be fulfilled + one of the other two is not something I'm thinking to add. It should be possible to do using Requirements callback + modifying talents' dependency values dynamically per talent! It's how I did it anyway.
 
Made a different version of TalentTreeViewModel that allows multiple panels at the same time (such as 3 of them in WoW).
The problem is that they don't share talent points, and I'm not going to implement it. It needs to be done custom. I can implement additional functionalities (such as tree-wide callbacks) that will make it easier.

It will be able to be used in the Setup trigger.

Update:
I decided to make it a stub method, by default it keeps internal state of points, but it can be overriden in the implementation. In this case, it uses "lumber resource" as talent points.
JASS:
method GetTalentPoints takes nothing returns integer
    return GetPlayerState(this.ownerPlayer, PLAYER_STATE_RESOURCE_LUMBER)
endmethod

method SetTalentPoints takes integer points returns nothing
    call SetPlayerState(this.ownerPlayer, PLAYER_STATE_RESOURCE_LUMBER, points)
endmethod

method Initialize takes nothing returns nothing
    local STKTalent_Talent t
    call this.SetColumnsRows(3, 4)
    set this.title = "Shepherd"
    call this.SetTalentPoints(6)
    ...
 
Last edited:
Okay I did some more features and I tried to make a representative example, to show people what can be made with it :p

I would say configuring this took me 2-3 hours
druidtalents.png

Functionally it works and everything. But I started feeling the drawbacks of jass itself, having tiny lags between actions like 0.05 seconds. During massive Cancel presses (like 30 or more unconfirmed points) it's even more present, maybe even 0.3-1 seconds or so.

The reason behind this could also be the amount of links (they are inefficient right now due to adding them just earlier and didn't optimize them, they also require some recursive calls due to how update works)

Confirming 30+ points doesn't make a delay however.
The "5 points per row" would have to be implemented through requirements.

In any case, I'm quite pleased how it works, in singleplayer it shouldn't be any trouble, and for few points here and there, shouldn't be any delay.
I have yet to start optimizing so I'll see how much performance I can get out of it.

I'm open to any questions.
 
Last edited:

SpasMaster

Hosted Project: SC
Level 22
Joined
Jan 29, 2010
Messages
1,949
This is looking better and better by the minute! :D

Would you mind attaching this version of yours with the Druid example to your post to mess with and explore?

Aside from that, personally, I feel like you could have the system display by default the maximum points that can be put into a talent (eg; 0/5), just like how it is in your Druid example and unlike how it currently is by default where it just shows the current points.
 
This is looking better and better by the minute! :D

Would you mind attaching this version of yours with the Druid example to your post to mess with and explore?

Aside from that, personally, I feel like you could have the system display by default the maximum points that can be put into a talent (eg; 0/5), just like how it is in your Druid example and unlike how it currently is by default where it just shows the current points.
Thank you :)

I will attach this version when I've cleaned it up a bit. In this version I'm importing talent tree frames from RUID output although modified a bit, and using a configured Talent (button etc) views in an easy to change form. So I would like clean examples of that to be available too.

I can leave the current/maximum rank showing as default, though because it's dependency injected, one can always switch between the two view generating functions.

I've played around a bit with the performance and it seems Cancel is the biggest culprit and I'll see how to resolve it. I've kind of fixed the micro delay on putting points by delaying the update with a timer, but Cancel is so ingrained in the system that it'll be hard to delay it without messing up the system code.

I'll see what I can do ^^

Edit:
It seems the Cancel was causing lag cause the talent trees kept redrawing themselves every time Talent Points changed (which is a LOT during Cancel).
I've extracted the redraw logic to the outside of the viewmodel, so I'm passing an onViewUpdate that will update the views in a different trigger context (using a timer)
 
Last edited:
Any chance you could port this to typescript or lua??
I have a typescript version which I wrote this one from, but I didn't publish it cause there was no interest. Also there's a very early lua version of it somewhere in the code lab.

In any case, publishing it would require some cleaning, making examples etc.

I'll gladly share it if anyone's ready to play around with it.
 
Level 3
Joined
Jul 21, 2020
Messages
45
I would have A LOT of interest in a typescript version. This is exactly the type of system I'm looking for in my thing and everything is in typescript already. One thing I haven't spent any time in is custom UI stuff so this would save me a ton of time.
 
Level 3
Joined
Jun 4, 2019
Messages
21
This my friend is absolut amazing! Ty for the druid example!

Edit: Little question aside, how could I make like in wow a requirment of points set in a line? For example u have to set 5 points overall to unlock the 2nd line, 10 points to unlock the 3rd line and so on
 
Last edited:
This my friend is absolut amazing! Ty for the druid example!

Edit: Little question aside, how could I make like in wow a requirment of points set in a line? For example u have to set 5 points overall to unlock the 2nd line, 10 points to unlock the 3rd line and so on

Hey,
well you would do it using the Requirements functionality. Make a function that calculates minimum points based on row of that talent, then return an error if it's lower than necessary. I can help if you need, you can find me on discord.

Good luck!
 
This looks amazing. Are you considering doing a GUI version?
Not any time soon, but I was considering/planning to make it possible to define talent trees in GUI, it would look something like this

  • Run trigger CreateTalentTree
  • // Talent 1
  • set variable TalentName = "Blah"
  • set variable TalentDescription = "Does this and that"
  • set variable OnActivate = Some GUI Trigger
  • set variable TalentCost = 2
  • set variable TalentX = 1
  • set variable TalentY = 2
  • Run trigger AddTalent
  • // Talent 2
  • set variable TalentName = "Blah2"
  • set variable TalentDescription = "Does this and that"
  • set variable OnActivate = Some other GUI Trigger
  • set variable TalentCost = 2
  • set variable TalentX = 1
  • set variable TalentY = 3
  • Run trigger AddTalent

Since that's the actual part where you "use" the system is by creating talent trees, and using the system should be easy.
But I don't intend on making the system customizable from GUI because the system itself is quite complex and it would do good for people to know code if they wanted to play around with how it works.
 
I mostly also use GUI but take a look at the examples (especially the druid examp), in my opinion they are so easy to read/reproduce, even I could use this system :grin:

Hey! That's the goal, at least out of the box, it should be clear, readable and easy to work with, regardless if you're an experienced coder or a casual GUI user.

Thanks :3
 
Top