• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Spellweaver's Talent Kitchen

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/Deactivation (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
  • Talent tree reset, deactivates all talents and refunds all points spent.
  • Save-Load API for integration with save-load systems

An API reference is included, and examples should provide more than enough information for implementation/trying things 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.
04.12.2021 - Sneakily added a typescript version here's the repo
23.03.2023 - Various minor fixes, added Talent reset, flexible talent icon methods, performance optimizations. Updated STK map (Shepherd version)
29.03.2023 - Updated Druid example map with changes
31.03.2023 - Added interface summary and API Documentation + fix talent reset tooltip not updating bug. STK and Druid map updated.
12.07.2023 - Fixed single rank dependency message not showing required rank, updated both STK and Druid map. Updated readme.md in git.
17.07.2023 - Added Save-Load API, updated both maps. This change affected most of the files (didn't touch Interfaces and Views folder) and added new ones (Services). Setup.j was completely reworked and consolidated between Druid and STK maps. TalentTree implementations (Shepherd, Balance, Feral, Restoration) should work as is, but require a slight change (registration) in order to utilize 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

        // Overriden stub methods, can delete this =================================
        // 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 GetTitle takes nothing returns string
        //     return this.title
        // endmethod
        // =========================================================================

        method Initialize takes nothing returns nothing
            local STKTalent_Talent t

            call this.SetIdColumnsRows(1, 3, 4)
            set this.title = "Shepherd"
            call this.SetTalentPoints(6)
            // set this.icon = "FireBolt"
            // TODO: set tree background texture here
            // set this.backgroundImage = "arms.dds"

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

            // Wondrous Flute <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Rank 1
            set t = this.CreateTalent().SetChainId(1)
            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.SetOnDeactivate(function thistype.Deactivate_Generic)
            call t.SetCost(0) // First level of this talent is free
            call this.AddTalent(0, 0, t)

            // Rank 2
            set t = this.CreateTalent().SetChainId(1)
            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 t.SetOnDeactivate(function thistype.Deactivate_Generic)
            call this.AddTalent(0, 0, t)
            // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Soothing Song <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Rank 1
            set t = this.CreateTalent().SetChainId(2)
            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.SetOnDeactivate(function thistype.Deactivate_Generic)
            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().SetChainId(2)
            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.SetOnDeactivate(function thistype.Deactivate_Generic)
            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().SetChainId(3)
            call t.SetName("Shepherd Apprentice")
            call t.SetDescription("Gain an apprentice.")
            call t.SetIcon("Peasant")
            call t.SetOnActivate(function thistype.Activate_GainApprentice)
            call t.SetOnDeactivate(function thistype.Deactivate_Generic)
            call this.AddTalent(2, 0, t)
            // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Lots of Apprentices <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Rank 1
            set t = this.CreateTalent().SetChainId(4)
            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.SetOnDeactivate(function thistype.Deactivate_Generic)
            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().SetChainId(4)
            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.SetOnDeactivate(function thistype.Deactivate_Generic)
            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().SetChainId(5)
            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.SetOnDeactivate(function thistype.Deactivate_Generic)
            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.CreateTalentCopy(t)
            call t.SetDescription("Gain 2 Sheep and 2 Flying Sheep.")
            call this.AddTalent(1, 1, t)

            // Rank 3
            set t = this.CreateTalentCopy(t)
            call t.SetDescription("Gain 3 Sheep and 3 Flying Sheep.")
            call this.AddTalent(1, 1, t)
            // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Call of the Wilds <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            // Rank 1
            set t = this.CreateTalent().SetChainId(6)
            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.SetOnDeactivate(function thistype.Deactivate_Generic)
            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
      
        // static method GetEventUnit takes nothing returns unit
        // Returns unit that owns the talent tree
        // thistype.GetEventUnit()

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

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

        // static method GetEventTalentTree takes nothing returns TalentTree
        // Returns "this"
        // local STKTalentTree_TalentTree tt = thistype.GetEventTalentTree()

        // static method SetTalentRequirementsResult takes string requirements returns nothing
        // Needs to be called within Requirements function to disable the talent
        // 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

        static method Deactivate_Generic takes nothing returns nothing
            local unit u = thistype.GetEventUnit()
            local STKTalent_Talent t = thistype.GetEventTalent()
            local integer r = thistype.GetEventRank()
            call BJDebugMsg("Deactivated " + t.name + " " + I2S(r))
        endmethod

        static method LoadCreate takes unit u returns Shepherd
            return thistype.create(u)
        endmethod
    endstruct

    private function init takes nothing returns nothing
        // Register Talent Trees
        call STKSaveLoad_RegisterTalentTree(1, Shepherd.LoadCreate)
    endfunction

endscope
Previews
Contents

Spellweaver's Talent Kitchen (Map)

STK Druid Talents (Map)

Reviews
Wrda
Must have been in STKTalentTree library (TalentTree script) DRUID version only: local player clickingPlayer variable is still leaking in TalentTreeViewModel script. By the way, cancel functionality is functioning like the "reset" in the other...

SpasMaster

Hosted Project: SC
Level 23
Joined
Jan 29, 2010
Messages
1,969
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 23
Joined
Jan 29, 2010
Messages
1,969
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 4
Joined
Jun 4, 2019
Messages
31
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!
 
Level 18
Joined
Oct 17, 2012
Messages
818
You can't access functions for frames in GUI, so most of the backbone code would be in custom script either way.

A GUI configuration would be possible with those set and get functions. And perhaps using real variables or triggers for events.
 
Last edited:
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
 
Level 11
Joined
Jul 4, 2016
Messages
626
Yes, in fact that's the default behavior. I had to change the example code so it would work like in WoW. Are you checking out the jass or ts version?

I'm willing to give an example but only if you ask
Using the jass version, I see, I thought that wasn't the case due to the druid example showing the same talent point used for all three trees.

I also saw in the druid example, you manually set up the UI stuff? So will it be the case that if one were to want to change the background, one would need to manually do so using the UI natives? I'm not familar at all with the new UI natives.
 
So will it be the case that if one were to want to change the background, one would need to manually do so using the UI natives?
You can definitely customize the way ui is rendered ("View" scripts) if you have the skills.

But if you only need to change the picture/background on the talent tree, then that's already part of the api. Check the druid example and see how it's done there

set this.backgroundImage = "balancebg.blp"
 
Last edited:
Level 11
Joined
Jul 4, 2016
Messages
626
Some suggestions to make this more versatile

1. Provide the ability to specific which talent tree and unit you wish to open for.
There are cases where a person may have multiple talent trees but not want them open at the same time.
For instance, an evolutionary tree would be fundamentally different from a ability talent tree.

2. Make the height and width of the talent tree configurable per talent tree.
 
Last edited:
Level 3
Joined
Aug 5, 2014
Messages
13
Hey this looks interesting, any chance to upload test map for non reforged users-1.31?
I tried porting it to test map but it crashes after 2 seconds in game.
 

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,864
Reference Leaks in TalentTreeViewModel -> CloseButtonClicked; CancelButtonClicked; ConfirmButtonClicked; TalentButtonClicked -> clickingPlayer variable.
Static method SetUpTriggersIfNeeded should be called in a "onInit" method than being constantly called at "create" static method. All of the above are included in the RUID version too.
It appears that in TalentViewModel the ActiveLinkTexture and TexturInactiveLinkTexture variables aren't uppercase as a whole, as the rest of the constants.
In TalentTree script, what exactly is the purpose of an empty library initializer? Is it for the user to utilize? (In RUID version too)
There's ActivateTalent method, following talent.onActivate, but no OnDeactivateTalent, nor talent.onDeactivate at all? (In RUID version too)
You can Simplify:
JASS:
method CheckDependencyKey takes integer requiredLevel, integer index, integer depIndex returns string
    local STKTalent_Talent talent = this.talents[index]
    local string errorText = null

    if (requiredLevel == 0 or talent == 0) then
        set errorText = null
    elseif (requiredLevel == -1) then
        set errorText = ""
    endif

    if (this.tempRankState[depIndex] < requiredLevel) then
        set errorText = errorText + this.talents[depIndex].name

        if (talent.maxRank > 1) then
            set errorText = errorText + " (" + I2S(requiredLevel) + ")"
        endif
    endif

    return errorText
endmethod
->
JASS:
method CheckDependencyKey takes integer requiredLevel, integer index, integer depIndex returns string
    local STKTalent_Talent talent = this.talents[index]
    local string errorText = null

    if (requiredLevel == -1) and not (requiredLevel == 0 and talent == 0) then //simplified the condition
        set errorText = ""
    endif

    if (this.tempRankState[depIndex] < requiredLevel) then
        set errorText = errorText + this.talents[depIndex].name

        if (talent.maxRank > 1) then
            set errorText = errorText + " (" + I2S(requiredLevel) + ")"
        endif
    endif

    return errorText
endmethod
JASS:
        // Grid starts from top left being (0, 0)
        method AddTalent takes integer x, integer y, STKTalent_Talent talent returns STKTalent_Talent
            return this.AddTalentRaw(x, y, talent)
        endmethod

        // Grid starts from top left being (0, 0)
        method AddTalentCopy takes integer x, integer y, STKTalent_Talent data returns STKTalent_Talent
            local STKTalent_Talent talent = STKTalent_Talent.createCopy(data)
            return this.AddTalentRaw(x, y, talent)
        endmethod
It starts from bottom left.
Talent script - It doesn't look a good idea for SetIcon method to have a prefix path. Not all icons have the same path logic (upgrades, passives, etc.)
It's not very clear what the whole API consists of, despite showing a good part of it in the example. Perhaps it would be nice to have a disabled custom script showing an API list (or a better way you have in mind).

RUID only version:
TalentTreeUIRuid - it would be nice if you fixed the indention here.
STKConstants - A lot of constants not following the standard rule, which you are already using. I think consistency is important.
JASS:
        private method AddTalentRaw takes integer x, integer y, STKTalent_Talent talent returns STKTalent_Talent
            local integer index = x + y * this.columns
            local STKTalent_Talent existing

            // If talent already exists
            if (this.talents[index] != 0) then

                // If talent is multirank, handle it differently
                if (this.talents[index].maxRank > 1) then
                    call this.talents[index].RemoveTalentFinalDescription()
                    call this.talents[index].SetLastRank(talent)
                    call this.talents[index].SetTalentFinalDescription()
                    call this.talents[index].UpdateMaxRank()
                else
                    call this.talents[index].SetLastRank(talent)
                    call this.talents[index].SetTalentFinalDescription()
                    call this.talents[index].UpdateMaxRank()
                endif
            else
                set this.talents[index] = talent
                set this.rankState[index] = 0
                call this.talents[index].UpdateMaxRank()
            endif

            if (talent.isLink) then
                set this.linkTalentIndex[this.linkTalentCount] = index
                set this.linkTalentCount = this.linkTalentCount + 1
            endif

            if (this.tempRankState[index] != talent.startingLevel) then
                set this.isDirty = true
            endif
            set this.tempRankState[index] = talent.startingLevel

            return talent
        endmethod
"existing" local variable is unused.
There are some "errors" in the examples. For example, "Balance", Improved Nature's Grasp's comments about rank keep being at rank 2. But that's a nitpick.
There seems to be a little spike when pressing the reset button after spending a considerable amount of talent points.

Overall, it's a pretty nice system to have, showing the beauty of the new natives. I would still like to see the major issues fixed first and then approve, although it's almost approvable.
 
Level 4
Joined
Jun 6, 2015
Messages
77
Trying to validate map triggers on test map but I get "Trigger 'HeroPickAssignTalentTree' has been disabled due to errors. This also goes for GiveHeroTalentPoint and OpenTalentTreeUI. Mostly unknown compile errors/symbol not declared.

It worked a patch or two ago though.
 
Level 11
Joined
Jul 4, 2016
Messages
626
Trying to validate map triggers on test map but I get "Trigger 'HeroPickAssignTalentTree' has been disabled due to errors. This also goes for GiveHeroTalentPoint and OpenTalentTreeUI. Mostly unknown compile errors/symbol not declared.

It worked a patch or two ago though.
Which version are you using? Lua or Vjass
If Vjass, enable Jasshelper and vjass in the drop menue
 
Level 4
Joined
Jun 6, 2015
Messages
77
Which version are you using? Lua or Vjass
If Vjass, enable Jasshelper and vjass in the drop menue
Well, now I fell very stupid.. Thanks anyway :)
I hadn't turned on JassHelper. I just saw Vjass was enabled but greyed out. My mistake.
I have used this system earlier, very good indeed!
 
Reference Leaks in TalentTreeViewModel -> CloseButtonClicked; CancelButtonClicked; ConfirmButtonClicked; TalentButtonClicked -> clickingPlayer variable.
Fixed, I think.

Static method SetUpTriggersIfNeeded should be called in a "onInit" method than being constantly called at "create" static method.
The reason why trigger creation is bound to constructor is mostly performance reasons, to avoid creating triggers in case there are no talent tree instances; i.e. there's no need for them. My assumption is that there might be 1-3 talent trees instantiated per player during the game so this function would not be called that many times, moreover, it by itself is very light (four null checks) after the first call.

I can make it a mandatory call in the setup though, it's probably a miniscule impact - I needed to explain reasons for this decision though.

It appears that in TalentViewModel the ActiveLinkTexture and TexturInactiveLinkTexture variables aren't uppercase as a whole, as the rest of the constants.
I think I named them that way because they are (currently unchangeable) static properties of TalentViewModel class and not global constants (they are accessed with "thistype.X") - their value is set to the global constants defined in the Constants.j; it's used internally and is not part of the system's configuration (which is an assumption that something being a global constant would imply). I wanted to make that distinction clear when naming them I think.

In TalentTree script, what exactly is the purpose of an empty library initializer? Is it for the user to utilize? (In RUID version too)
It's not there for user to initialize, it's part of the library internal code. There's no need for it, but it also explicity tells the reader that nothing happens on init.

There's ActivateTalent method, following talent.onActivate, but no OnDeactivateTalent, nor talent.onDeactivate at all? (In RUID version too)
Talent Deactivation would be used in "Reset talents" mechanic, which was not supported. Talent class itself does support defining its onDeactivate, but it's not being triggered in any way. (no reset talents button or function after they've been activated). I've added the reset mechanic now, it's triggered by an ability in the STK example map.

You can Simplify:
JASS:
method CheckDependencyKey takes integer requiredLevel, integer index, integer depIndex returns string
local STKTalent_Talent talent = this.talents[index]
local string errorText = null

if (requiredLevel == 0 or talent == 0) then
set errorText = null
elseif (requiredLevel == -1) then
set errorText = ""
endif

if (this.tempRankState[depIndex] < requiredLevel) then
set errorText = errorText + this.talents[depIndex].name

if (talent.maxRank > 1) then
set errorText = errorText + " (" + I2S(requiredLevel) + ")"
endif
endif

return errorText
endmethod
->
JASS:
method CheckDependencyKey takes integer requiredLevel, integer index, integer depIndex returns string
local STKTalent_Talent talent = this.talents[index]
local string errorText = null

if (requiredLevel == -1) and not (requiredLevel == 0 and talent == 0) then //simplified the condition
set errorText = ""
endif

if (this.tempRankState[depIndex] < requiredLevel) then
set errorText = errorText + this.talents[depIndex].name

if (talent.maxRank > 1) then
set errorText = errorText + " (" + I2S(requiredLevel) + ")"
endif
endif

return errorText
endmethod
Making this change would break the logic, since there's two outcomes based on its conditions. The smell here is that I am returning twofold information in this single string; Error exists/not exists, and the error message itself. It's unintuitive and risky, but due to jass' limitation, I'd have to write a bunch more (less performant) boilerplate as an alternative. I prefer this to that. CheckDependencyKey function is the most sensitive mechanic in the whole system, I would rather not touch it unless really needed.

Example, this is part of logic this change would break. (TalentTree.j line 344)
if (this.CheckDependencyKey(reqLevel, i, depIndex) == null) then
set level = level + 1
endif

It starts from bottom left.
Tried to search for this text, couldn't find it. The issue seems to be already fixed in git, this is probably an issue in one of the example maps. I fixed it in the STK map.

Talent script - It doesn't look a good idea for SetIcon method to have a prefix path. Not all icons have the same path logic (upgrades, passives, etc.)
The idea here was to minimize the effort and force consistency by adding the limit. It also makes the consumer/content code more clear and easier to read, which I think is very important. I added two methods SetIconEnabled and SetIconDisabled for those who need more control and flexibility.

STKConstants - A lot of constants not following the standard rule, which you are already using. I think consistency is important.
Changed constants name to all follow UPPER_SNAKE_CASE

There seems to be a little spike when pressing the reset button after spending a considerable amount of talent points.
Changed the way it's rendered, avoiding multiple recursive updates, performance increased a lot.


I didn't manage to get down all the points, will have to revisit RUID version individually.
All the changes can be seen in this pull request. Feature/review update 101222 by Gohagga · Pull Request #3 · Gohagga/STK_vJass
 

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,864
Tried to search for this text, couldn't find it. The issue seems to be already fixed in git, this is probably an issue in one of the example maps. I fixed it in the STK map.
Must have been in STKTalentTree library (TalentTree script)

DRUID version only:
local player clickingPlayer variable is still leaking in TalentTreeViewModel script.

By the way, cancel functionality is functioning like the "reset" in the other version.

Performance seems better overall. Although there's no proper documentation, the examples show several features of the system enough to be a approved. A documentation is expected nonetheless.

Approved
 
I updated the Druid map with all the new changes and features.

local player clickingPlayer variable is still leaking in TalentTreeViewModel script.
With updated Druid map, this issue should be fixed now.

By the way, cancel functionality is functioning like the "reset" in the other version.
I renamed "Reset" into "Cancel" within Druid map for consistency, and since "Reset" now refers to resetting the tree.

A documentation is expected nonetheless.
I'm taking my time so I can finish it properly.

Also fixed RUID file indentation.

And this is it for now.
 
Level 4
Joined
Apr 22, 2022
Messages
31
Amazing work! Been playing around with the system (Shepherd map) and haven't found anything really wrong with it, but noticed that talent descriptions on multi-rank talents dont reset when you reset the talents via Custom script: call STK_ResetUnitTalentTree(GetTriggerUnit()) It seems the way to reset the descriptions is to temporarily put enough points into the talent and then hitting the cancel button. Also noticed if a talent that is single rank depends on another talent (single or multi-rank), it wont show the required rank of dependency in brackets in the description. Minor things but still worth noting.
 
Last edited:
Amazing work! Been playing around with the system (Shepherd map) and haven't found anything really wrong with it, but noticed that talent descriptions on multi-rank talents dont reset when you reset the talents via Custom script: call STK_ResetUnitTalentTree(GetTriggerUnit()) It seems the way to reset the descriptions is to temporarily put enough points into the talent and then hitting the cancel button. Also noticed if a talent that is single rank depends on another talent (single or multi-rank), it wont show the required rank of dependency in brackets in the description. Minor things but still worth noting.
Thank you very much for the bug report :D
Uploaded versions with documentation and I also fixed the "Reset" issue you are mentioning thanks to your insight.
I haven't looked into dependency error text message issue yet but I will at some point.
 
Level 4
Joined
Apr 22, 2022
Messages
31
Sadly the latest version you just released has introduced a new bug, when you reset talents you get back twice as many talent points. Example you have 10 talent points spent and confirmed, then reset and you get 20 talent points back.
 
I tried to install it on a fresh map and noticed that Setup.j has this line:
STKTalentTreeViewModel_TalentTreeViewModel.create(Player(i), talentTreeView, GenerateTalentSlot, 1)
While the TalentTreeViewModel.create takes in 5 parameters, the last one is ViewChanged onViewChanged where it is defined as:
function interface ViewChanged takes TalentTreeViewModel ttvm, player watcher returns nothing

Basically, seems like the code on github result in compile error because of the above mentioned reason.
 
I tried to install it on a fresh map and noticed that Setup.j has this line:
STKTalentTreeViewModel_TalentTreeViewModel.create(Player(i), talentTreeView, GenerateTalentSlot, 1)
While the TalentTreeViewModel.create takes in 5 parameters, the last one is ViewChanged onViewChanged where it is defined as:
function interface ViewChanged takes TalentTreeViewModel ttvm, player watcher returns nothing

Basically, seems like the code on github result in compile error because of the above mentioned reason.
Hey, github has code for ~3 different maps so I might not have tested it with latest changes.

Look at the Examples folder, they should have more correct versions of Setup.j

Or you can send the last param as -1
 
So, I've managed to get the example to run on Warcraft 3 version 1.31.1.
What's causing issues for this version is SetUpLink in TalentView, specifically the BlzFrameSetLevel call. From my understanding, this isn't really all that important to set and removing that line would make it compatible with 1.31.

Also, for some reason, there are a few commented // call this.ResetTalentViewModels(), and if you don't have the one in TalentClicked method, it doesn't update the UI as you click. Maybe that the others are good to have too, haven't really gotten that far yet.

But hey, neat system and it almost does exactly what I want, so cool!

What I want is 4 "choose 1 out of X" "talents", one unlocked per boss, but shouldn't be too hard to make it happen with this system :)

Fairly tiny thing, but there is a timer leak and not nulled timer in the setup.j when init calles GameBeginningSetup.
 
So, I've managed to get the example to run on Warcraft 3 version 1.31.1.
What's causing issues for this version is SetUpLink in TalentView, specifically the BlzFrameSetLevel call. From my understanding, this isn't really all that important to set and removing that line would make it compatible with 1.31.

Also, for some reason, there are a few commented // call this.ResetTalentViewModels(), and if you don't have the one in TalentClicked method, it doesn't update the UI as you click. Maybe that the others are good to have too, haven't really gotten that far yet.

But hey, neat system and it almost does exactly what I want, so cool!

What I want is 4 "choose 1 out of X" "talents", one unlocked per boss, but shouldn't be too hard to make it happen with this system :)

Fairly tiny thing, but there is a timer leak and not nulled timer in the setup.j when init calles GameBeginningSetup.

Awesome to hear you are managing, I'm sorry it's not working out of the box. Not many users like you so it's not ironed out just yet.
I will see if I can make some changes you are suggesting next time I get to it.

I've been making a bunch of changes lately and while I did make the map examples work fully, I did not completely update the Setup.j files in git though it seems.

If you need any help with using or extending it (it's extendable :p) throw me a direct message, or hit me up on hive discord (I'm more available there)
 
Also noticed if a talent that is single rank depends on another talent (single or multi-rank), it wont show the required rank of dependency in brackets in the description. Minor things but still worth noting.
Sorry it took me a while, but I found and fixed this issue. I was in fact, checking max level of the talent in question to avoid displaying the "(rank)", instead of checking the max level of the talent it depends on. From what I've tested, it works now (updated maps and in git).

Basically, seems like the code on github result in compile error because of the above mentioned reason.
Now that I took a better look, you might have looked in the main branch which wasn't up to date, all the changes I've been doing were in the feature/reviewUpdate_101222. Though I've merged it now so it shouldn't happen again.
 
A little tease, am finally working on save-load API. Also consolidated some things that were strange/different between Shepherd map and the druid one, I put some order into Setup.j files.



First and foremost, the API is for serializing and deserializing unit's one or more talent trees to and from a bit string. Then this bit string can be handled in one's map in whichever way you need/want/do already. In the example above, I also encoded them for easier testing.

I was considering to write out of the box integration with commonly used save-load systems, but I'd need to research those first.

Code:
-> function RegisterTalentTree takes integer id, TalentTreeFactory factoryMethod returns boolean
    Should be called on init for every type of TalentTree implementation. This must be done so that system knows which TalentTree to
    create for the unit when loading
    call STKSaveLoad_RegisterTalentTree(1, Shepherd.LoadCreate)
 
-> function LoadForUnit takes unit owner, string bitString returns nothing
    Takes the bitStream string, creates TalentTree instances for the unit, loads the ranks and activates talents
    call STKSaveLoad_LoadForUnit(udg_Hero, udg_BitStream)
 
-> function SaveForUnit takes unit owner returns string
    Takes the unit's TalentTree objects and creates a bit string with rank data
    call STKSaveLoad_SaveForUnit(udg_Hero)

I played around a lot with trying to reduce the size, and came up with this dynamic structure that depends on constant defined thresholds.
public constant integer TALENTTREE_ID_THRESHOLD_SMALL = 16
public constant integer TALENTTREE_ID_THRESHOLD_LARGE = 64
public constant integer TALENT_ID_THRESHOLD_SMALL = 32
public constant integer TALENT_ID_THRESHOLD_LARGE = 128
public constant integer TALENT_RANK_THRESHOLD_SMALL = 16
public constant integer TALENT_RANK_THRESHOLD_MEDIUM = 32
public constant integer TALENT_RANK_THRESHOLD_LARGE = 64
public constant integer TALENT_RANK_THRESHOLD_EXTRA = 256

So, there's 2 sizes for talent tree Id, 2 sizes for talent Id and 4 sizes for talent rank ids. This means as your map grows, so does amount of bits required to store various information in the save code. The idea is to keep it as small as possible while keeping backwards compatibility.
Also, it adjusts the size according to usage needs. Some people have 10 talents with 1000 ranks each, and some have 100 talents with 1 rank each. This makes it so that there's not too much overhead in either case.

Of course, the idea is that user sort of knows these thresholds/boundaries and to keep backwards compat, they should never be changed during development of the map. Of course, they can be changed, but that is very likely to break backwards compatibility. You could always play it safe and save everything as integers, doing it the old fashioned wc3 way, put 2,147,483,647 into those constants. 🤪 I have no idea how big the code would be at that point, probably around 7-10 times longer.

Cheers

Update: Ended up releasing it very soon. Major changes, likelihood of bugs. Sorry! 😅
 
Last edited:
Top