• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

Help With My Turn Based Combat System

E

Exlo

Guest
This will be my first post here so, first of all, hello and thanks for taking the time to read about my problem. I'm really sorry if it's a bit of a read.

I'm very new to GMS and coding in general, so I've of course jumped straight into something way out of my league by trying to create a turn based RPG. I've learned a lot over the course of these 2 months and truly believe I can do it, but there must be something fundamental I'm misunderstanding here. This is my third crack at this, and keep in mind I'm likely dreadfully disorganized and probably have some terrible habits code-wise. Any tips to remedy these issues are more than welcome. I will try to explain the system as concisely as possible, but keep in mind I've never really asked for help yet, so I'm new even to this. I'm willing to share the project file if necessary. The exact nature of the issue is that characters don't seem to even do anything on the combat screen, though I think I've finally figured out how to reference all the data and use all the structures provided properly. However, if anyone can clearly explain accessors in the context I am using them, it would be much appreciated. I think I understand them finally but when I would see examples of use it had me confused more often than not.

So first player character data objects are created in the start menu, and put into an object that will later be manipulated to manage inventories and possible party members. Right now I'm just filling the variables with unique instance IDs on creation of those party members, and it's literally just an object with three vars. Ally1, ally2, ally3. Each character data object is persistent and looks like this.

Code:
// PORTRAIT
portrait = spr_player_portrait;

// STATS
av_experience = 0;

av_max_health = 100;
av_current_health = 100;

av_max_mana = 25;
av_current_mana = 25;

av_max_stamina = 25;
av_current_stamina = 25;

av_max_strength = 5;
av_current_strength = 5;

av_max_endurance = 5;
av_current_endurance = 5;

av_max_intelligence = 5;
av_current_intelligence = 5;

av_max_wisdom = 5;
av_current_wisdom = 5;

av_max_agility = 10;
av_current_agility = 10;

av_max_ego = 5;
av_current_ego = 5;

av_max_piety = 5;
av_current_piety = 5;

av_max_luck = 3;
av_current_luck = 3;

av_max_accuracy = 0.5;
av_current_accuracy = 0.5;

av_max_evasion = 0.1;
av_current_evasion = 0.1;


// MODIFIERS
//Modifier remove scripts stored here (cycle array, if != undefined decrement that mods round count, then check if 0)
mod_is_applied = array_create(mod_types.length, undefined);

//Modifier count (if zero: remove flag and effect on new round, if other: subtract by 1)
mod_round_count = array_create(mod_types.length, 0);

//Holds values saved by mod skills to refer to on removal
mod_magnitudes = ds_grid_create(mod_types.length, effect_types.length);

//Actual mod values
av_experience_mod = 0;

av_health_mod = 0;
av_mana_mod = 0;
av_stamina_mod = 0;

av_strength_mod = 0;
av_endurance_mod = 0;
av_intelligence_mod = 0;
av_wisdom_mod = 0;
av_agility_mod = 0;
av_ego_mod = 0;
av_piety_mod = 0;
av_luck_mod = 0;

av_accuracy_mod = 0;
av_evasion_mod = 0;

//Go ahead and add the player to position one in list manager
obj_player_list_manager.ally1 = id;
Enemy objects are exactly the same. Basically, we step on a tile to start an encounter, that tile passes an encounter script to another persistent object, and then the room changes to rm_Combat. Now the combat manager is made. This object should be handling everything that's going on in the scene, and it is created before anything else in the room.

Here is the create event.
Code:
//Disallow player input until player turn
global.p_combat_input = false;

//Hold encounter script
encounter_scr = obj_encounter_manager.script;

//Hold current action to execute
current_action = undefined;

//Hold character currently inputting action
current_actor = undefined;

//Hold current target for actions
current_target = undefined;

//Hold number of rounds passed this combat
current_round = 0;

//Hold current member of combat having mods checked
mod_cleanup_target = undefined;

//Party array
//0-2 are ally
//3-8 are enemy
party_array = array_create(9, undefined);

party_array[0] = obj_player_list_manager.ally1;
party_array[1] = obj_player_list_manager.ally2;
party_array[2] = obj_player_list_manager.ally3;

//Set up enemies in scene and in party_array
script_execute(encounter_scr);

//Holds list of characters in combat ordered by agi
turn_order = array_create(9, undefined);

//Used to read positions of turn_order and return current actor
turn_pos = 0;

//Calculate the turn order
calculate_turn_order();

//Initiate new round
new_round = true;
And here is the step event.
Code:
if (new_round)
{

//Cycle through mods and remove them if round count is 0
for (var _ii = 0; _ii < 9; _ii += 1)
{
    mod_cleanup_target = turn_order[_ii];
    if (mod_cleanup_target != undefined)
    {
        //Cycle through THAT ACTORS mods
        for (var _jj = 0; _jj < mod_types.length; _jj += 1)
        {
            //If mod IS applied
            if(mod_cleanup_target.mod_is_applied[_jj] != undefined)
            {
          
            //Remove a round from that mods round count
            mod_cleanup_target.mod_round_count[@ _jj] -= 1;
          
            //If that mods round count = 0, execute the script in mod_cleanup_target.mod_is_applied
            if (mod_cleanup_target.mod_round_count[_jj] == 0)
            {script_execute(mod_cleanup_target.mod_is_applied[_jj]);}
          
          
            }
        }
    }
}

current_round += 1;
turn_pos = 0;
new_round = false;

}

//First check if turn order position is undefined, if it is we just go ahead and increment pos
if (turn_order[turn_pos] != undefined)
{

    //Check if it's enemy or player turn
    if (turn_order[turn_pos] == party_array[0] || party_array[1] || party_array[2])
    {
  
    //Set current actor to member of player party
    current_actor = turn_order[turn_pos];
  
    //On player turn first allow player input (set this to false in skill scripts)
    global.p_combat_input = true;
  
    //If current action is filled we will execute its contents
    if (current_action != undefined)
    script_execute(current_action);
  
    //Reset current action
    current_action = undefined;

    }
  
    else
    {
      
    //Reset current target here to help AI
    current_target = undefined;
      
    //Set current actor to member of enemy party   
    current_actor = turn_order[turn_pos];
  
    //If its an enemy's turn we will need to have their ai_package determine a target and actions
    script_execute(turn_order[turn_pos].ai_package);
  
    //Execute action
    script_execute(current_action);
  
    //Reset current action
    current_action = undefined;
  
    }

}

else
turn_pos += 1;

//If we have incremented to the total number of possible characters, start new round
if (turn_pos == 8)
new_round = true;
Now I'll show you the scripts I'm using in the thing. This is where I think I'm having issues, most likely in calculating the turn order but I just can't be sure if my method isn't working or what. The encounter script just places enemy instances (using instance create layer) in positions with objects returning their IDs to the party_array on collision.

Here's calculate_turn_order.

Code:
//Clear original turn_order array
for (var _j = 0; _j < 9; _j += 1)
{

turn_order[@ _j] = undefined;

}

//Create grid to hold references and their agi
var _order_grid = ds_grid_create(9, 2);
ds_grid_clear(_order_grid, -1);

//Fill grid
var _char = undefined;

if (party_array[0] != undefined){
_char = party_array[@ 0];
_order_grid[# 0, 0] = _char;
_order_grid[# 0, 1] = _char.av_current_agility;
}

if (party_array[1] != undefined){
_char = party_array[@ 1];
_order_grid[# 1, 0] = _char;
_order_grid[# 1, 1] = _char.av_current_agility;
}

if (party_array[2] != undefined){
_char = party_array[@ 2];
_order_grid[# 2, 0] = _char;
_order_grid[# 2, 1] = _char.av_current_agility;
}

if (party_array[3] != undefined){
_char = party_array[@ 3];
_order_grid[# 3, 0] = _char;
_order_grid[# 3, 1] = _char.av_current_agility;
}

if (party_array[4] != undefined){
_char = party_array[@ 4];
_order_grid[# 4, 0] = _char;
_order_grid[# 4, 1] = _char.av_current_agility;
}

if (party_array[5] != undefined){
_char = party_array[@ 5];
_order_grid[# 5, 0] = _char;
_order_grid[# 5, 1] = _char.av_current_agility;
}

if (party_array[6] != undefined){
_char = party_array[@ 6];
_order_grid[# 6, 0] = _char;
_order_grid[# 6, 1] = _char.av_current_agility;
}

if (party_array[7] != undefined){
_char = party_array[@ 7];
_order_grid[# 7, 0] = _char;
_order_grid[# 7, 1] = _char.av_current_agility;
}

if (party_array[8] != undefined){
_char = party_array[@ 8];
_order_grid[# 8, 0] = _char;
_order_grid[# 8, 1] = _char.av_current_agility;
}

for (var _i = 0; _i < 9; _i += 1){
    
//Find greatest agility value
var _max_agi = ds_grid_get_max(_order_grid, 0, 1, 8, 1);

//Find x position of the greatest agility value
var _max_agi_x = ds_grid_value_x(_order_grid, 0, 1, 8, 8, _max_agi);

//Send that reference from the grid to the front of the turn order
turn_order[@ _i] = _order_grid[# _max_agi_x, 0];

//Set that reference's agi to -1 and search again
_order_grid[# _max_agi_x, _max_agi] = -1;


}

//Clean up grid
ds_grid_destroy(_order_grid);
In the step event, here is the enemy ai script deciding the action script.
Code:
//When choosing target remember 0-2 is player party and 3-8 is enemy
while (current_target == undefined)
current_target = party_array[@ irandom(3)];


//If target is player party
if (current_target == party_array[@ 0] || party_array[@ 1] || party_array[@ 2])
{
current_action = basic_action_attack;
}

else
current_action = basic_action_defend;
And here are our action scripts. Attack.
Code:
//Calculate base damage
var _base = current_target.av_current_endurance - current_actor.av_current_strength;

current_target.av_current_health += _base;


//End skill
global.p_combat_input = false;

turn_pos += 1;
And defend (much more complex due to me trying to set up this modifier system).
Code:
//Calculate base effect magnitude
var _end_base = current_actor.av_current_endurance * 2;
var _wis_base = current_actor.av_current_wisdom * 2;

//Add this to appropriate values
current_actor.av_endurance_mod += _end_base;
current_actor.av_current_endurance += _end_base;
current_actor.av_wisdom_mod += _wis_base;
current_actor.av_current_wisdom += _wis_base;

//Now add this to the magnitude grid for the current actor (this is checked so we know what values to remove
//on remove_mod_defend, in this case)
current_actor.mod_magnitudes[# mod_types.defend, effect_types.endurance] = _end_base;
current_actor.mod_magnitudes[# mod_types.defend, effect_types.wisdom] = _wis_base;

//Add mod info to current_actor
current_actor.mod_is_applied[@ mod_types.defend] = remove_mod_defend;
current_actor.mod_round_count[@ mod_types.defend] = 1;

//End skill
global.p_combat_input = false;

turn_pos += 1;
I'm sorry this is so much to look at. I've just gotten so frustrated trying to figure all of this out and want to so bad, especially considering once I'm over this hump a lot of the other systems won't even be this complex -- or they won't need to be anyway. I've been over it so many times and can't figure out where things aren't clicking.

Keep in mind my code isn't throwing errors, I just know the enemies aren't dealing damage to the player party. Enemy strength is higher than all the player parties endurance so I should be seeing something happen. I'm tracking the health right know with just simple objs drawing text. I guess I'll post one of those too.
Code:
draw_self();

var _owner = obj_player_list_manager.ally1;

var _max = _owner.av_max_health + _owner.av_health_mod;
var _current = _owner.av_current_health;

draw_text(x + (bar_width / 2), y + (bar_height / 2), string(_current) + "/" + string(_current));
I know it references the data properly because the correct numbers appear when combat starts. Anyway, thank you for taking the time to read that. I really really appreciate any help or guidance. Ask me anything if you think I've left something out.
 
Code:
draw_text(x + (bar_width / 2), y + (bar_height / 2), string(_current) + "/" + string(_current));
I don't know about anything else...but that is drawing the same variable twice :)

I may be wrong, but if you're only going by what you see on screen (as proof that it is failing) - is it possible that is the entirety of your mistake?
 
Do you know how the debugger works? Honestly, that's -way- too much code for most people to go through and figure out, plus you've abstracted a lot of it into forms that are relatively difficult to follow without being able to mind read. Just as an example:
Code:
var _base = current_target.av_current_endurance - current_actor.av_current_strength;
current_target.av_current_health += _base;
Why are you doing the damage this way? It seems to me as though if the target has higher endurance than the strength of the actor, the target will get actively healed from the attack? (if target has end 10 and actor str 9, 10-9 = 1 so av_current_health += 1 is the result...) Seems like a confusing mechanic on the surface. In any case, click on the first line in that codeblock that I quoted inside of GM to get the text cursor on it and then press F9. This will set a breakpoint. Then run the game using the F6 key and when damage is applied, the game will pause and you will be able to step through the code line by line in the debugger and actually -watch- what is happening as damage is being applied. Read the manual on the debugger if you're confused as to how. Learning to use the debugger will help you be able to figure out exactly what is happening as your code runs and is -completely- necessary when you start working with more complex code like this. There comes a point where only you will be able to know how your code works once your project gets large enough/complex enough and you will have to develop the necessary skills to debug it yourself.
 
E

Exlo

Guest
Thank you very much for these answers guys. So firstly, I have no idea how I botched up the health bar code like that, but it was just one of those moments I guess. But I fixed that and still don't see any changes to my numbers here, so that's frustrating. Second, yeah the attack code is not what it's going to be in the end of course. I was just trying to get it to execute and see a change so that I know it's working (enemy strength is higher than any party members end right now), but as you've stated I really should learn to use the debugger in that case. So thank you for telling me how!

So it looks like the first issue is that, at the point we get to making an attack, the ids stored in turn_order are all identical. So that tells me the method I'm using for calculating turn order isn't working (just jamming the same one 9 times). Then it looks like references aren't properly being passed into the party_array, except for the three player characters. That to me makes me think I've misunderstood something about arrays, but I'm not sure. Sorry, I know it's a lot to look at, but I wasn't sure exactly how to word my question or provide accurate information otherwise. Like I said, I'm probably in too deep without enough knowledge, I just don't want to get discouraged and abandon everything. I was mostly hoping people would gloss over some glaring issues with what I'm doing that I hadn't noticed myself. And I guess you guys did, so that is a good thing.

Thank you for posting this tutorial also! I had been looking for videos on turn based systems but I don't think any of them had what I was looking for. I'll be sure to take a look at these.
 

Rob

Member
Thank you very much for these answers guys. So firstly, I have no idea how I botched up the health bar code like that, but it was just one of those moments I guess. But I fixed that and still don't see any changes to my numbers here, so that's frustrating. Second, yeah the attack code is not what it's going to be in the end of course. I was just trying to get it to execute and see a change so that I know it's working (enemy strength is higher than any party members end right now), but as you've stated I really should learn to use the debugger in that case. So thank you for telling me how!

So it looks like the first issue is that, at the point we get to making an attack, the ids stored in turn_order are all identical. So that tells me the method I'm using for calculating turn order isn't working (just jamming the same one 9 times). Then it looks like references aren't properly being passed into the party_array, except for the three player characters. That to me makes me think I've misunderstood something about arrays, but I'm not sure. Sorry, I know it's a lot to look at, but I wasn't sure exactly how to word my question or provide accurate information otherwise. Like I said, I'm probably in too deep without enough knowledge, I just don't want to get discouraged and abandon everything. I was mostly hoping people would gloss over some glaring issues with what I'm doing that I hadn't noticed myself. And I guess you guys did, so that is a good thing.

Thank you for posting this tutorial also! I had been looking for videos on turn based systems but I don't think any of them had what I was looking for. I'll be sure to take a look at these.
They're a bit lengthy and if you're doing well on your own then you might not need to watch them at all. I just figured it could give you some pointers if you're struggling.

I always try and get the structure of a battle system done first before adding anything else. In terms of your turn order, if you're comfortable using ds_lists then I found it a good way to do it. I just reset the list(s) every time it was the start of the player/AI and then add relevant characters to the list (dead/stunned/asleep characters don't get their names on the list - they're not coming in!), deleting them from it at the start of their turn. The single Player/Monster battle system didn't need it but the Multiple Hero/Monster one did.
 
After using GM for about 5 years I'd still class myself as a noob, and not a particularly good programmer, but you might see some merit in this advice.....

All it takes is a break missing from a switch statement somewhere, or a code bracket in the wrong place, and you're looking at a bit of a nightmare figuring out why something isn't working. That is, at least, when you have a lot of code in effect, or have several segments that are feeding into one end result. And of course that is based on the assumption that you've coded it correctly in the first place - rather than it being a typo creeping in somewhere.

It's tempting to go full steam ahead when you have an idea for how the code will work, and it sounds like you done that here. I know just enough to build some gameplay systems, and will dive right into it when "inspiration" hits me. But without stopping to check until I have the end result (if that's possible before it's "done") I often find myself spending just as much time, if not more, addressing niggly little things I've overlooked in my "enthusiasm".

What you perhaps should be doing is coding it in increments. You put in one system, and check that it works. You put in another, check that it works, and that it hasn't broken the previous system, and so on, and so on....That you have got to this stage, and the fundamentals aren't working as expected, suggests that you either didn't do this, or that you don't quite understand some of the functions.

One other thing to bare in mind: when something is working, don't just accept it. You have to see if you can break it by deliberately doing as many possible circumstances that you can think of. Because that one little thing you overlooked could go right through everything else. That's the other thing that has cost me huge amounts of time - checking the code for typo's / checking the code for wrongly ordered events / maybe its a permanent variable and I've repeated it somewhere with the same variable defined as local, leading to complications....and after doing all of that it ends up being one tiny outcome I haven't put in a solution for.

As for giving up.....I might be a tad bloody minded, but I've spent the last four years working on my AI, and have gone through probably at least 10 different iterations of it using any technique no matter how far out it might seem. When an attempt did work it would be another uphill struggle to get it running well in terms of the framerate, so then it would be "what parts, if any, can I take that might work better and combine them", and the process continued...painfully and slowly :)

I asked someone who achieved pretty good results before to do this for me - it took them about a week to plan it out, seemed quite viable that they could do it, and they wanted a four figure sum in payment....A week!...That amount of money!....versus me fumbling around for four years...more cash left in my wallet, but with less hair from me pulling it out in frustration. Fun times! But I'm still at it.

Stuff you try might not work out, but hopefully you'll at least learn something from any mistakes you make. And everybody has to start somewhere :)
 
E

Exlo

Guest
First the list idea is a good one. I think I'm just going to go back over things, rework the system. Try to take it step by step, maybe start with a simpler implementation. Maybe forget ordering by agility for now, that might help. Thank you guys for your advice. I think maybe some of this was exactly what I needed to hear. c:
 

Rob

Member
First the list idea is a good one. I think I'm just going to go back over things, rework the system. Try to take it step by step, maybe start with a simpler implementation. Maybe forget ordering by agility for now, that might help. Thank you guys for your advice. I think maybe some of this was exactly what I needed to hear. c:
If you wanna order by agility, you can just use ds_list_sort
 
Top