• Content Count

  • Joined

 Content Type 




Klei Bug Tracker

Game Updates

Hot Lava Bug Reporter

Posts posted by rooks

  1. Targeting intents was something we experimented with in development, but was ended up not being used and got disabled.  There's a few places in the code where this is done explicitly and it's unlikely to work completely without some edits and play-testing.

    If you'd like to experiment, Minigame:CanPlayCard is responsible for the invalid target message (lines 1496-1498).   You'd also need to type check the 'primary_target' variable in Minigame:CollectTargets.




    • Like 1
    • Thanks 1

  2. Ah in that case I will amend AddCombatPartyDef like this:


    function AddCombatPartyDef(id, fn)
        if GENERATORS[id] then
            print (loc.format( "Combat party generator {1} is already defined. If you aren't reloading combat, look into this", id ))
        GENERATORS[id] = fn

    As for the helper functions, the best I can do is make them global.  Which ones were you referring to specifically?  PickTeams, FindValidTeams, etc.?

    • Thanks 1

  3. When playing under the English language setting, only fonts that support English characters are loaded, so Chinese characters for example will not be displayed correctly.

    You can possibly try providing an override to the English language settings in your mod's OnPreLoad function, something like this:

    local function OnPreLoad()
            id = "english",
            name = "English (Notosans)",
            font_settings = 
                title = { font = "fonts/", sdfthreshold = 0.36, sdfboldthreshold = 0.30, sdfshadowthreshold = 0.25 },
                body = { font = "fonts/", sdfthreshold = 0.4, sdfboldthreshold = 0.33 },
                button = { font = "fonts/", sdfthreshold = 0.4, sdfboldthreshold = 0.33 },
                tooltip = { font = "fonts/", sdfthreshold = 0.4, sdfboldthreshold = 0.33 },
                speech = { font = "fonts/", sdfthreshold = 0.4, sdfboldthreshold = 0.33 },
            default_languages = 


    Not sure if that answers your question but hopefully it helps.


    • Thanks 1

  4. Any luck?  Which messages did you receive instead of the ones you expected?  From your post I can't immediately see what might be going wrong, it seems to be set up okay.

    If you hit tilde (~) when receiving the bonuses in conversation, and then click on the "Quips" button, you will be able view which quips were recently looked up, as below.

    The words in cyan show which tags were used to lookup a quip.  In this case, chum_bonus and player_sal are the salient ones.  In the DB section below, only one match was found, and so Fssh uses that quip (NEVER NEVER).  In your case, there should be player_victor instead of player_sal.


    • Thanks 1

  5. Ah ok.  So because the counter attack is actually just an attack being issued directly via counter_attack:RunAttack(), there is no post resolve step and so the burn condition won't be added.  Post resolve only happens when a card is played via BattleEngine:PlayCard.  What you can do is simply add the burn condition to the attack directly in the PC_WUMPUS_hot_shot condition -- in fact if you do this, you can use the regular counter_attack card and don't even need to define PC_WUMPUS_hotshot_att.

    Below I pasted a copy of your code with the relevant 2 changes:


        PC_WUMPUS_hot_shot =
            name = "Hot Shot",
            desc = "At the end of your turn, take damage equal to stacks of {WUMPUS}. When attacked, apply {BURN} to the attacker, equal to stacks of {WUMPUS}.",
            ctype = CTYPE.DEBUFF,
            event_handlers =
                --remove the condition at the start of your turn, not the end of it.
                [ BATTLE_EVENT.BEGIN_TURN ] = function( self, fighter )
                    if fighter == self.owner then
                        self.owner:RemoveCondition( )

                [ BATTLE_EVENT.ON_HIT] = function( self, battle, attack, hit )
                    --this is the card the ai plays.
                    local resolve_card = attack.card
                    --if the card player is active and doesn't have stun or isn't surrendering...
                    if self.owner:IsActive() and not self.owner:HasCondition( "STUN" ) and not self.owner:HasCondition("SURRENDER") then
                        --if the card owner is them self and the owner wasn't "called in" by certain cards (I.E. Oolo type shenanigans) and it's an attack card and it deals damage and it isn't...a counter? what?
                        --Okay I think the is_counter is just so the game doesn't loop counters. Makes sense.
                        if resolve_card.owner and resolve_card.owner ~= self.owner and not resolve_card.owner.call_in and resolve_card:IsAttackCard() and hit.damage and (hit.damage > 0 or (attack.card.max_damage and attack.card.max_damage > 0)) and not attack.is_counter then
                            --if the counter owner is the target
                            if == self.owner then
                                self.counter_attack_counter = (self.counter_attack_counter or 0) + 1
                                attack.riposted = true

                [ BATTLE_EVENT.END_RESOLVE ] = function( self, battle, resolve_card )
                    --if the counter counter is above 0
                    if self.counter_attack_counter and self.counter_attack_counter > 0 then
                        --If the person with counter is active, and not stunned, and isn't surrendering, and the attacker is also active.
                        if self.owner:IsActive() and not self.owner:HasCondition( "STUN" ) and not self.owner:HasCondition("SURRENDER") and resolve_card.owner:IsActive() then
                            --This is the actual card, I believe
                            local card = Battle.Card( "counter_attack", self.owner )
                            card.engine = battle -- HAX
                            card.burn_amt = self.stacks
                            --Get the owner's fight data, and check if they have ranged_riposte
                            local fight_data = self.owner.agent.fight_data
                            if fight_data.riposte_tags then
                                card.hit_tags = self.owner.riposte_tags
                            --Flavour, just updates the card flags so it can be ranged or melee
                            if fight_data.ranged_riposte then
                                card.flags = CARD_FLAGS.SPECIAL | CARD_FLAGS.RANGED
                            --Play the card against the attacker
                            card:AssignTarget( resolve_card.owner, { { resolve_card.owner } } )
                            --For every point in the counter counter
                            for i=1, self.counter_attack_counter do
                                --If the fu-ah, uh..."nice person" hasn't keeeeeee I am really bad at being family friendly in my code annotations, apparently. over yet then
                                if resolve_card.owner:IsAlive() then
                                    --Get the card again, then attack 
                                    local counter_attack = Battle.Attack( card )
                                    --set it to true so the game doesn't endlessly run the attack...that's a good trick, actually.
                                    counter_attack.is_counter = true
                                    counter_attack:AddCondition( "BURN", self.stacks ) -- Apply BURN directly.
                                    --Do the visual coolness, like the actual attacks.
                                    battle:BroadcastEvent( BATTLE_EVENT.ON_RIPOSTE, counter_attack, i == 1, resolve_card, i == self.counter_attack_counter )
                            --did the guy who just got brutalized decide this ain't worth dying for?
                    --The counter is perpetually at 0 in the background, at least until the counter owner gets hit.
                    self.counter_attack_counter = 0
                [ BATTLE_EVENT.CONDITION_ADDED ] = function( self, fighter, condition, stacks, source )
                    if fighter == self.owner and condition:GetID() == "STUN" then
                --My own snippet. Hotshot is a risk, but one you can manage for Big Damage.
                [ BATTLE_EVENT.END_PLAYER_TURN ] = function( self, fighter, stacks )
                    self.owner:SelfDamage( self.stacks, self )


    • Like 1
    • Thanks 1

  6. Can you explain in more detail what problem you are running into?

    The game treats English in a special way -- any strings built directly into the definitions of the game's content (cards, characters, etc.) will be treated as the de facto English translation.  If you used Chinese strings, the game will probably treat these as the "english" version.  So what you will need to do is to use English strings in the definitions of your content, and instead use a translation file (.po) that contains all the Chinese strings.

    I believe there are a number of mods that support both English and Chinese, you might use one of those as reference.

    Good luck!

    • Thanks 1

  7. I managed to reproduce the crash in the debugger, but unfortunately the callstack is indeed deep in the FMOD DLL and we don't have the source code to give any more hints  Sometimes it crashes when unloading banks, and sometimes it happens when loading the regular game bank after a reset.  I imagine the modded banks/loading multiple banks are violating some unspoken FMOD assumption about how they should be structured.  Sorry I couldn't be of more help.


    • Thanks 1
    • Sad Dupe 1

  8. Official mod support for audio won't be forthcoming at this time.  Regarding the crash, I imagine something about having multiple audio banks loaded is crashing FMOD.  I apologize as I realize that's not very helpful, but I'm not too familiar with the FMOD internals myself.  If you manage to release a copy of your audio mod, I could try loading it in the debugger to see if the crash is reproduceable.


    • Thanks 1

  9. 3 hours ago, RageLeague said:

    (Oh that's why. I was wondering whether it's my mod or whether it's the base game's problem. I can't really tell anything from the crash screen.)

    (It seems that if you have a convo and never end it with a cxt:End or something like that, it causes the game to be very confused.)

    (Also, if you disable democratic race, existing saves might not work because I also added closed jobs to several location defs, and the game doesn't like loading a non-existing job, even if no one works at that job.)

    I'll update the game to handle non-existent WorkPositions without complaining.  Should be in next experimental.

    • Thanks 4

  10. 14 hours ago, RageLeague said:

    Is there like a OnDestroy function that doesn't just check if the destruction is a bounty claim? This can be useful with something like the appropriated modifier, where if they get removed from having no stacks the cards are gone forever.

    OnUnapply, if it exists, will get called whenever the modifier is removed in whatever fashion.

    • Thanks 2

  11. On 9/22/2020 at 12:52 PM, Scrumch said:

    As in making a whole new outfit for one of the characters.

    The process for making an entire outfit involves a couple of external tools and scripts that aren't included in the game unfortunately.  I imagine with some skilled reverse engineering it would be possible, but for now it's a bit beyond the scope of what's possible with the existing mod tools.

    • Thanks 2

  12. Overview

    This is a very brief set of instructions on how to create a new empty mod for Griftlands.  This is not a tutorial and assumes basic knowledge of Lua.  The best way to learn how to add specific pieces of content to your mod is to subscribe to Shel's Adventure and Havarian and view the code.  These example mods illustrate many basic features, like adding cards, grafts, and new campaigns.


    Running the game in debug

    Running the game in debug will unlock a slew of facilities to aid you in development.  It is not strictly necessary to run in debug to either run or develop mods, but it will probably be helpful.

     Epic -> Go to Settings > Griftlands and check "Additional Command Line Parameters".  In the edit box, enter --debug.
     Steam -> Right click Griftlands in your library and click "Properties".  Click "Set Launch Options", enter --debug in the edit box and click Ok.

    A description of all the game's debug functionality is beyond the scope of this article, but for our purposes, it is useful to know that CTRL+5 will open a debug panel listing any mods the game has discovered.  Once you create a mod or (eventually) install other user-created mod content, you will see their appearance here.  You can enable or disable them from this panel.

    Creating an empty mod

    Browse to your Griftlands save directory.
      Windows/Steam -> %APPDATA%/Klei/Griftlands/steam-<steamID>/
      Windows/Epic -> %APPDATA%/Klei/Griftlands/<EpicID>/

    If you see a saves/ directory, a log.txt, and other game-generated files, you will know you are in the right place!  Create a new folder named 'mods' if one doesn't already exist.  This folder houses all local mod content.

    Inside the 'mods' folder, create another folder with the name of your mod.

    Inside that folder, create a text file called modinit.lua.  This file will be executed when your mod is installed and enabled.  This file should return a table which will serve as your mod's run-time state.  There are a few properties it can have which have special meaning.  Here are some of them:

      OnLoad: this must be a function that receives one parameter, your mod's table.  This function is called by the game after loading all core game content and is where your mod can load its own assets and game content.

      OnPreLoad: this must a be a function that receives one parameter, your mod's table.  This function is similar to OnLoad, but is called BEFORE the game loads all its game content.  In general, the only reason to use OnPreLoad is to load .po files for translations, because these need to be in memory before the rest of the game content is loaded for string lookup.

      alias: This is an optional (but useful) string which specifies a filepath alias by which your mod can reference files within its directory.

      title: This is the name that will represent your mod when you see it in game. When you upload your mod to Workshop for the first time, it will be named after this string.

      description: This is a description of what your mod is. When you upload your mod to Workshop for the first time, it will receive this description.

      previewImagePath: This is a path to a filename of a .png within your mod directory that will be used as the when your item is seen in the Workshop.

    Sharing Your Mod

    One way to share your mod is simply to distribute your folder to players.  They can then use the above process to copy your mod in to their local mods/ folder.

    If you are playing Grifltands on Steam, the preferred way is to use Steam Workshop to distribute your mods, and also download other mods.  Note that the mods distributed through Steam Workshop do not appear in your local user folder, but in a special Steam folder on your computer.

    Uploading to Steam Workshop

    To share a mod that you have locally in your settings folder, you must first enable --debug mode.  Then, hit CTRL+5 to open the Mod inspector.  In this panel you should see your own mod listed.

    Click the "Upload to Workshop" button to upload your mod to Steam!

    The first time your mod is uploaded, you may notice a new file called steam_workshop.txt in your mod folder.  You should keep this file, as it references the workshop id of your mod.  The next time you upload to Workshop, this file is used to update the same workshop item you previously uploaded.

    Deleting your mod from Workshop

    If you no longer wish to distribute your mod, you can Delete it by signing into Steam Workshop and clicking the "Delete" option under Owner Controls.

    Note that any user which has already subscribed and downloaded your mod will continue to have access to it, until they Unsubscribe.

    If you wish to later redistribute your mod again, you will need to ensure you are creating a NEW workshop item for it.  To do this, delete the auto-generated file steam_workshop.txt in your mod folder.  This will ensure a new workshop item is generated the next time you upload.  If you try to Upload your mod with the old workshop id that was previously deleted, you will receive an error when Uploading.


    Example Mods

    To get started, a good way is to download the example mods from Steam Workshop and inspect their contents.  One is the Havarian mod, which should serve as a simple example of adding new language support.  The other is called Shel's Adventure, and has examples of other types of supported mod content.  (I should make clear that this mod is not meant to be a fully playable campaign, just an implementation example)



    • Like 3
    • Thanks 1

  13. 1 hour ago, RageLeague said:

    also, this update broke my mods. again. the way to reference files from the mod folders changed. now how are we going to reference files in the new system?


    Sorry for the inconvenience.  I do try to avoid breaking compatibility but since this is our big push for initial mod support there's likely to be a few changes coming down the pipe.  You can still reference files within your mod using aliases, but the preferred way to do it is within the OnLoad or OnPreLoad function.  At that point, there will be a filepath alias mounted automatically based on the mod id (in your case, DEMOCRATICRACE).  You can specify a different alias (alias = "DEMOCRATIC_RACE", eg.) in the table returned by modinit.lua.

    In the meantime I restored the old behaviour to MountMountData so things should work normally, but the function is likely to go away by the time the update arrives.

    If you run in to any more mod issues, lemme know in the mod thread as I will be monitoring that are regularly.


    • Like 2
    • Thanks 1