Need help with very simple turn-based combat

(edit)

As someone new to Quest, I've been happy at how I've figured out lots of things, but this one has stumped me. After much trial and error (and reading!) I've decided to bite the bullet and ask for help!

I've searched the forums here and found lots of posts discussing making combat systems, but their all either end up relying on a library (which is fine, but I've not found one that does what I want), diverging into complex code discussions, or being unresolved.

I'm looking do implement a very simple turn-based combat, the sort of thing found in gamebooks like the Lone Wolf series or Fighting Fantasy.

Basics:
The player and enemies have a Skill and Endurance score. The former is their combat ability, the latter their "hit points". I can set all that up just fine.

Now, here's how I imagined the system working:

  1. Player enters a room where an enemy has been placed (object with an "ishostile" boolean set to true)
  2. Combat initiates automatically (no need to type "fight enemy", etc.)
  3. The player is asked "What do you want to do?", and is presented with the choices "Fight", "Special", "Escape". This can be via an inline or popup menu (the latter is preferable).
  4. When the player chooses "Fight", a function runs that compares player skill + 1D10 random roll vs enemy skill + 1D10 random roll. If the player is lower, they lose 2 Endurance. If the enemy is lower, they lose 2 Endurance.
  5. If neither party is dead (Endurance 0 or less), combat loops to the next "round", and the player is asked to Fight/Special/Escape again. This continues until someone is dead or "Escape" is chosen.

"Special" would use an item to instantly kill the enemy. "Escape" would end combat and move the player back to a specific room. That can wait for later to figure out, though.

At this point, I don't even need the enemy and player to alternate "turns"; for now, combat can just be one set of "dice rolls" to determine a "winner" and "loser" each "round".

I'm also not worried so much at figuring out the mechanics for combat (endurance loss, dice rolls, escaping, etc.). I think I can manage that myself with some work.

Making the combat auto-trigger is presumably a matter of using something like "foreach (object, ScopeReachableNotHeldForRoom (player.parent)) { if (HasAttribute(object, "hostile")) { if (object.hostile = true) { etc" which I can get working with a turnscript.

What I DO really need help with is how to do the basic "Combat Starts->Select Fight->Do Combat->Repeat if nobody is dead" script loop/function.

I've tried using ShowMenu (or the popup equivalent) but keep getting stuck on either ShowMenu not parsing variables (so I had to turn them into global game.x ones, I suspect?) or not being able to make the menu "pause" to show the player messages relating to combat.

I'm sure something basic like this is doable with a lot less headache than it's giving me, so I'd really appreciate if anyone can help me figure this out. If there's actually a "turn based combat" library, awesome! But I'm happy to learn, if only I can get the basic menu flow working.

Note: below is my most recent attempt, but I still can't get it working (no feedback messages are shown, and I suspect game.fightoption isn't being changed no matter what the user clicks when asked).

	game.fightoption = 0
	menulist = NewStringList()
	list add (menulist, "Fight")
	list add (menulist, "Special")
	list add (menulist, "Escape")
	ShowMenu ("You are in combat.", menulist, false) {
	  if (result = "Fight") {
		game.fightoption = 1
	  }
	  if (result = "Special") {
		game.fightoption = 2
	  }
	  if (result = "Escape") {
		game.fightoption = 3
	  }
	}
	if(game.fightoption=1) {
		msg ("You fight the foe!")
	}
	else if(game.fightoption=2) {
		msg ("You can't do that now.")
	}
	else if(game.fightoption=3) {
		msg ("You can't escape this fight.")
	}
	else if (game.fightoption=0) {
		msg ("Error! fightoption = "+game.fightoption+"!")
	}

Please disregard the above code if starting from scratch is better and less confusing!


your code is almost 100% correct (though you might want to remove the 'dot/period' in your "You are in combat." ShowMenu's display prompt Parameter, as it might cause problems (probably don't need to remove it, but just in case, as the dot/period can be parsed/seen as a coding command/symbol, which would cause problems), and you can just use an 'else' for the fallback-catch last condition:

	game.fightoption = 0

	menulist = NewStringList()
	list add (menulist, "Fight")
	list add (menulist, "Special")
	list add (menulist, "Escape")

	ShowMenu ("You are in combat", menulist, false) {

	  if (result = "Fight") {
		game.fightoption = 1
	  }
	  else if (result = "Special") {
		game.fightoption = 2
	  }
	  else if (result = "Escape") {
		game.fightoption = 3
	  }
	}

        // the 'on ready' is for hopefully being able to make the below wait, until you've selected your choice from the menu above, but it doesn't always work... but the explanation (someone explained it in some post) confused me at the time anyways of what/when it works and what/when it doesn't... so the 'on ready' might not do any good anyways...

        on ready {
	  if (game.fightoption=1) {
		msg ("You fight the foe!")
	  }
	  else if (game.fightoption=2) {
		msg ("You can't do that now.")
	  }
	  else if(game.fightoption=3) {
		msg ("You can't escape this fight.")
	  }
	  else {
		msg ("Error! fightoption = "+game.fightoption)
	  }
        }

as for 'looping', here's a simple example:

(do NOT do this though, as this is an infinite loop, which will crash quest, infinite loops are bad, lol)

<game name="example_game">
  <attr name="start" type="script">
    infinite_loop_function
  </attr>
</game>

<function name="infinite_loop_function">
  msg ("hi")
  infinite_loop_function
</function>

the 'game.start' fires and calls/does/uses the 'infinite_loop_function' Function

the 'infinite_loop_function' Function' displays 'hi', and then calls/does/uses the 'infinite_loop_function' Function (itself) again

and thus you got an endless displayment of:

hi
hi
hi
hi
hi
hi
hi
etc etc etc

however, your computer's, and quest's use of its, resources (memory/space) are not infinite, and so quest crashes


to not have an infinite loop, you need a way to stop/exit/end it from calling itself (looping)

for example:

<game name="example_game">
  <attr name="start" type="script">
    escapable_loop_function
  </attr>
</game>

<function name="escapable_loop_function">
  msg ("hi")
  ask ("loop?") {
    // the built-in 'ask/Ask' Functions/Scripts:
    // choice: yes ---> result = true
    // choice: no ---> result = false
    if (result) { // if (TRUE)
      escapable_loop_function
    } else { // if (FALSE)
      msg ("BYE BYE!")
    }
  }
</function>

for the combat auto-trigger:

the simpliest way would be to use the global 'onenterroom' Script:

'game' Object -> WHATEVER TAB -> find the global 'onenterroom' Script (or it's called something like 'upon entering room, run a script' or something like that)

and then, you're right, you'd use the 'foreach' and an Object List (which can be created/returned via the Scope Functions) to iterate through all of the Objects in the Room you're in, checking if they got your 'ishostile' Boolean Attribute, and if they do, then handle the combat for them... you can do the combat individually for each of them or you can have your combat handle all of them together, let us know which you prefer, and we can help with either design/method you want to do


P.S.

are you using the 'Game Book' or the 'Text Adventure' version of quest?


you might have already seen this combat code of mine and it confuses you, but if not, here it is:

https://textadventures.co.uk/forum/quest/topic/3348/noobie-hks-help-me-thread#22485 (download pertex' file, as he cleaned up this old bad poor combat code of mine, so you can understand it better)

and there's this too, but it's a bit more advanced (and you'd need to be able to understand/read code... or you can try to load it into quest's GUI/Editor and study it there, and/or play/run it... but it's old code of an earlier quest version, so it probably won't work for current version of quest):

http://textadventures.co.uk/forum/samples/topic/4988/character-creation-crude-code-and-sample-game

you can look at it for some ideas/ways of doing (looping) menus (most of it is redundent functions --- my brain wasn't working at the time... as I can have a single/few functions handling all of it instead of the many redundent functions... lol)


When using ShowMenu, you need to remember that all it does is display a menu, and make an internal note of what to do when to do when the player chooses an option.

ShowMenu ("This is displayed on the screen", some_options, true) {
  // Quest *makes a note of* this code, to run when
  // the player has chosen an option
}
// This code is run immediately after displaying the menu

So at the moment, you're trying to display "You fight the foe" or similar before the player has chosen an option.

You need to do something more like:

menulist = NewStringList()
list add (menulist, "Fight")
list add (menulist, "Special")
list add (menulist, "Escape")
ShowMenu ("You are in combat.", menulist, false) {
  if(result = "Fight") {
    msg ("You fight the foe!")
  }
  else if(result = "Special") {
    msg ("You can't do that now.")
  }
  else if(result = "Escape") {
    msg ("You can't escape this fight.")
  }
  else {
    msg ("Error! result = "+result+"!")
  }
  if ( some test to see if the enemy is still alive ) {
    DoFight
  }
}

(assuming this code is in a function called DoFight; so it runs it again if the enemy isn't dead)


I'll try to contribute.
I have game code myself. You could have just typed "simple rpg combat" in the search bar, like I did...

My code posted. https://textadventures.co.uk/forum/samples/topic/qzqe52enhuuacpdrljnffq/simple-turn-rpg-combat-for-the-web-quest#e27e61b1-4658-4a0f-bea4-2d01f7f325c0
Second thread as update. http://textadventures.co.uk/forum/samples/topic/vg6jtjrayesr4e5kqqytig/simple-combat-code-update-working-on-a-new-system#700b753e-07e5-40dc-9fff-6c7cfa19ab28

This is The Pixie's Zombie Apocalypse tutorial. It uses a turnscript instead of anything else, kind of like a real first person shooter! This may be what you want. https://github.com/ThePix/quest/wiki/The-Zombie-Apocalypse-(on-the-web-version)

Just don't forget attributes if you're doing combat!
I also have another game where the monsters spawn 100% the first time, then they spawn again after a counter ticks the right number. I couldn't figure out how to do it the traditional way, so I made one counter and one attribute. I realize I could have only did it with the counter, but it's old business now. Basically, the monsters would only spawn on one number only. Ask me if you are interested in it, and I'll post it up.

As for spawning enemies, I believe I created an entire lock system for my first game, which literally locks all rooms at once. My second game (titled Fing Game) uses a teleport system which teleports the player to another room, the battle room. (Post link for context. http://textadventures.co.uk/forum/quest/topic/tyotvy93i02ca5efbxpuyg/teleporting-a-player-and-a-zombie-stallin )


Thanks to everyone who replied! I just hope my ramblings make sense; I'm not a programmer, so I worry I'm able to even convey what I'm attempting or wanting, much less do it correctly!

hegemonkhan, thanks for all the detailed examples and suggestions; I've been impressed with your work on this forum from all the threads I read through before asking for help myself!

mrangel, that's exactly where I hit my biggest stumbling block: putting the code for each menu choice inside the menu didn't work for me because variables have to all be global for that to work. Putting it afterwards... yup, ShowMenu doesn't pause for input! In the end, I went back to my original model, similar to your example, and converted all my variables needed into global ones for use during combat (i.e. each enemy has their own "stats" but these get translated to global game.xxx variables for use in combat). Not sure if that's the best way to do it, but it solves the variables-in-menus issue for me.

For now, I've held off going back to getting the "auto-combat" part working and just focused on the actual combat cycle for now. In my current build, combat begins when the player types "fight xxx" when an enemy is present. And it works! I can fight and kill or be killed, all in turn-based gamebook style combat (note: this is text adventure mode, not gamebook mode, though).

I've got two problems still, however. While combat now works (yay!), I wanted to add a "Special" submenu for spells, special attacks, grenades, etc. Broadly speaking, this means the player can either Fight (normal combat), Special (special submenu) or Escape (this depends on if escape is allowed or not by the enemy). The first and last work, but the Special part is proving problematic for one main reason: I can't get menus to run inside another menu correctly! It's obvious what's happening: the menus both run together, rather than one "pausing" while the other interrupts.

Combat should look like this:
You are in combat. Select action:

  1. Fight
    ->If 1, player fights the enemy. Loser takes damage.
  2. Special
    ->If 2, show submenu:
    ---1. Grenade
    ---2. Heal (not implemented)
    ---3. Cancel
    ---->These do special actions; grenades automatically hit enemy for 1d10 damage, cancel goes back to original combat menu.
  3. Escape
    -> Ends combat unless enemy is set to disallow this.
    Combat repeats if enemy and player are still alive.

I'll post my code below and would appreciate pointers on how to fix it!

Enemies

  <type name="combatenc">
	<displayverbs type="listextend">Fight</displayverbs>
    <inventoryverbs type="listextend">Fight</inventoryverbs>
    <fight type="script">
		DoFight(this,this.name,this.combatskill,this.endurance,this.wpnbonus,this.wpndamage,this.noescape)
    </fight>
    <autocombat type="boolean">false</autocombat>
    <combatskill type="int">1</combatskill>
    <endurance type="int">1</endurance>
    <wpnbonus type="int">0</wpnbonus>
    <wpndamage type="int">1</wpndamage>
    <noescape type="boolean">true</noescape>
  </type>

Combat Functions

<function name="DoFight" parameters="enemy,enemyname,enemycombatskill,enemyendurance,enemywpnbonus,enemywpndamage,enemyescape">
    <![CDATA[
    game.incombat = true
	game.enemy = enemy
    game.enemyname = enemyname
    game.enemycombatskill = enemycombatskill
    game.enemyendurance = enemyendurance
    game.enemywpnbonus = enemywpnbonus
    game.enemywpndamage = enemywpndamage
    game.noescape = enemyescape
	UpdateEnemyStatus()
	ClearScreen
	menulist = NewStringList()
	list add (menulist, "Fight")
	list add (menulist, "Special")
	list add (menulist, "Escape")
	ShowMenu ("You are in combat", menulist, false) {
	  if(result = "Fight") {
	    msg ("You fight the "+game.enemyname+"!")
		RollCombat()
		CheckPlayer()
		CheckEnemy()
	  }
	  else if(result = "Special") {
   		DoSpecial()
	  }
	  else if(result = "Escape") {
		if(GetBoolean(game, "noescape")) {
	    	msg ("You cannot escape from this combat.")
		}
		else {
	    	msg ("You escape from combat.")
	    	game.incombat = false
	    }
	  }
	  wait {
	  	if ((game.enemyendurance > 0) and (GetBoolean(game, "incombat"))) {
	    	DoFight(game.enemy,game.enemyname,game.enemycombatskill,game.enemyendurance,game.enemywpnbonus,game.enemywpndamage,game.noescape)
	  	}
	  	else {
		  	if (game.enemyendurance < 1) {
				RemoveObject (game.enemy)		  
		  	}
	  	  	ClearEnemyStatus()
	  	}
	  }
	}
    ]]>
  </function>

Special Function

  <function name="DoSpecial">
    <![CDATA[
	spmenulist = NewStringList()
	if(GetBoolean(game, "allowgrenades")) {
		list add (spmenulist, "Grenade")
	}
	list add (spmenulist, "Heal")
	list add (spmenulist, "Cancel")
	ShowMenu ("What do you want to do?", spmenulist, true) {
	  if(result = "Grenade") {
		UseGrenade()
		CheckEnemy()
	  }
	  else if(result = "Heal") {
	    msg ("You can't do that now.")
	  }
	  else if(result = "Cancel") {
	  }
	}
    ]]>
  </function>

The "wait" command in DoFight is very possibly wrong and likely part of the problem, but that's the only way I could get combat to pause at all, so message feedback could be read before the next "round".

Right now, it gives a nice "Continue" after normal Fight or Escape choices, but while it allows Special choice to work, it doesn't really work because there's still a "Continue" link underneath that can be clicked (skips submenu, as expected) and there's no user textbox to input the choice number; that is, while I can use the Grenade option by clicking on the hyperlink for it, I can't type "1" in that menu like I can for Fight (presumably, the "menu shown while inside a menu" is confusing things?).

Note: Enemy stats aren't shown in messages in the code above because it's being displayed in a status pane that updates during combat.

As I said, my core combat now works great for what I wanted... it was when I tried to add the "Special" submenu (I've started with just one working choice: Grenade) that I ran into trouble again! :(

P.S. jmnevil54, I never found your thread, though the Pokemon battle menu concept sounds a lot like what I want to do! Sadly, these forums seem to have a terrible search system; i.e. I'd searched for "basic combat" and "turn-based" already! :( I will definitely take a look at your game/code; it looks fun, too!

The zombie apocalypse example is awesome but much more of a "live/active combat" method compared to the static gamebook style I want. In fact, I've already been working on something like that for a different game, with combat running "live" via turnscripts, which I found surprisingly easier than the turn-based type due to not having to mess with menus and "pausing"!


"basic combat" and "turn-based"

I guess that might be because my games or my threads don't have those words (maybe turn based, I don't know) in the m. I'll take a look.

What do you have going on for that looping combat in the first place? You may need to turn that off! For the time being, turn off the looping, and maybe then let the player decide when they attack?


'mrangel, that's exactly where I hit my biggest stumbling block: putting the code for each menu choice inside the menu didn't work for me because variables have to all be global for that to work (Banjo)'

sorry about that... I have a hard time remembering/knowing whether 'Variable' VARIABLES (NAME_OF_VARIABLE or NAME_OF_VARIABLE = VALUE_OR_EXPRESSION) are able to be used inside of nested scripting stuff or not.

Since it doesn't work, the reason is that the 'ShowMenu/show menu' Scripts/Functions is walled off from the outside scripting.

'Attribute' VARIABLES (NAME_OF_OBJECT.NAME_OF_ATTRIBUTE or NAME_OF_OBJECT.NAME_OF_ATTRIBUTE = VALUE_OR_EXPRESSION) are able to bypass this 'walling off', and thus why they're 'global' VARIABLES, as you can use them anywhere

you can 'pass/transfer' through Parameters but the built-in 'show menu/ShowMenu' Scripts/Functions have 3 Parameters set already, and can't take anymore (which you could use to transfer your 'Variable' VARIABLE into it for use by it), unless you edit its Function.

this is why its a good idea to learn and just use 'Attribute' VARIABLES for now, until you understand more about basic coding/scripting and are thus better in understanding how to use 'Variables' VARIABLES with that basic knowledge in coding/scripting


i.e. each enemy has their own "stats" but these get translated to global game.xxx variables for use in combat). Not sure if that's the best way to do it, but it solves the variables-in-menus issue for me (Banjo)

you can just directly use your individual Objects' stats ('Attribute' VARIABLES) in your combat code, unless you're doing stuff in your combat code that would alter those stats' values and you don't want that to happen, if this is the case, then you actually already did it the correct way, by assigning those stats into your combat scripting's 'Variable' VARIABLES, as this way, you're not altering your stats' values.


as for your issues:

your code is actually perfect (for the most part / as far as I can see)

the issue you're having is dealing with the 'order of operations' with your code design and how the quest code stuff works

you can try sticking in the 'on ready' Script/Function where-ever it might be needed to hopefully work and wait for the prior code/scripting to finish, but it doesn't always work (it depends on some complicated stuff that I had trouble understanding, lol):

http://docs.textadventures.co.uk/quest/scripts/on_ready.html

otherwise, a trick is to use a Boolean VARIABLE with an 'if' Script, but this only half solves the problem... as if the scripting is still progressing to get to this point, it'll pass by this point (thanks to the 'if Boolean VARIABLE' we put in, but then you're past where you want to be... it gets a bit complicated... we'd need to handle it based upon the code you got, or come up with some better code and handle it with it.


while you're new to coding...

it's best to try to keep your scripting in its simplest form, which is:

do stuff 1
-> do stuff 2
->-> do stuff 3
->->-> do stuff 4
->->->-> do stuff 5
etc etc etc

this is a "straight" line of (and via nested) operations:

do stuff 1 -> do stuff 2 -> do stuff 3 -> do stuff 4 -> do stuff 5 -> (etc etc etc)


as for an example, look at how the 'order of operations' work with this:

do stuff 1
|
|-> do stuff 2
|
do stuff 3
|
do stuff 4

both 'do stuff 2' and 'do stuff 3' are firing at (technical: nearly, unless they're using separate 'threads'/CPUs and thus are able to be simultaneus) the same time, and on top of that, the 'do stuff 3' can finish before the 'do stuff 2' finishes, and thus the 'do stuff 4' is firing while the 'do stuff 2' still hasn;t finished


try this (I just added in the 'on ready' before your 'wait') simple attempt-fix (lol):

(otherwise, we'll have to re-design all/some of your various combat codings, so the order of operations works as wanted/needed)

<function name="DoFight" parameters="enemy,enemyname,enemycombatskill,enemyendurance,enemywpnbonus,enemywpndamage,enemyescape">

  <![CDATA[

    game.incombat = true

    game.enemy = enemy

    game.enemyname = enemyname

    game.enemycombatskill = enemycombatskill

    game.enemyendurance = enemyendurance

    game.enemywpnbonus = enemywpnbonus

    game.enemywpndamage = enemywpndamage

    game.noescape = enemyescape
    
    UpdateEnemyStatus()

    ClearScreen
    
    menulist = NewStringList()
    list add (menulist, "Fight")
    list add (menulist, "Special")
    list add (menulist, "Escape")

    ShowMenu ("You are in combat", menulist, false) {

      if (result = "Fight") {

        msg ("You fight the "+game.enemyname+"!")

        RollCombat()

        CheckPlayer()
        CheckEnemy()

      } else if (result = "Special") {

        DoSpecial()

      } else if (result = "Escape") {

        if (GetBoolean(game, "noescape")) {

	  msg ("You cannot escape from this combat.")

	} else {

	  msg ("You escape from combat.")

	  game.incombat = false

	} // end of 'else'

      } // end of 'else if'

      on ready { // this was your 'wait { /* scripting */ }'

        wait { // new 'wait' here instead, to keep this functionality for you

          if (game.enemyendurance > 0 and GetBoolean (game, "incombat")) {

            DoFight (game.enemy,game.enemyname,game.enemycombatskill,game.enemyendurance,game.enemywpnbonus,game.enemywpndamage,game.noescape)

	  } else {

            if (game.enemyendurance < 1) {

	      RemoveObject (game.enemy)		  

	    } // end of 'if'

	    ClearEnemyStatus()

	  } // end of 'else'

        } // end of 'wait'

      } // end of 'on ready'

    } // end of 'ShowMenu'

  ]]>

</function>

Wow, thanks hegemonkhan... but sadly, that didn't seem to make a difference. I was hoping I could use on ready elsewhere maybe but reading up about it, it seems to not work with ShowMenu (I'm actually kinda wondering if using the popup "show menu" script might work, from reading that link... I think it might look better too).

The obvious solution is just to put all the commands that would be in Special in the main combat menu (conditionally, if the player has those items/skills) but that feels like admitting defeat. Quest must be able to do nested menus (sub-menus) somehow!

jmnevil54: that's what I've done; I've not bothered getting the auto-triggering of combat working and in my test build, I have to click (or type) "fight" for my test enemy to begin combat. I can already get combat to auto-start if I want it to, but I had issues with multiple combatants (again, the solution is not to have these, as they're not needed for gamebook style combat really, but I wanted to see if I could make it work with two or more enemies eventually).

Right now, though, I just want to get this issue with menus nailed down... as I said, my programming experience isn't much (done a lot of modding with various languages over the years, but never formally learned coding) but this is my first time hitting something where it's clearly the order things are being done that's screwing me up; I suspect because it's rather counter-intuitive for ShowMenu not to pause as one might expect before continuing (judging from the many similar comments I've read here looking for an answer) and the need to work around this.


I think this is how it works:

ShowMenu("CHOOSE!", Split("choice one;choice two", ";"), true){
  // Everything here happens AFTER making this choice
  // You cannot use local variables from outside of this ShowMenu here!
  // You can only use object attributes (which are basically global variables)
}
// Anything here will happen without waiting for ShowMenu!
// on ready does not wait for ShowMenu!

show menu ("CHOOSE!", Split("choice one;choice two", ";"), true){
  // Everything here happens AFTER making this choice
  // You cannot use local variables from outside of this show menu here!
  // You can only use object attributes (which are basically global variables)
}
on ready {
  // This will not run until AFTER show menu
}
// Anything here will happen without waiting for show menu!


show menu is a Quest script, which means it's in the C# code (which we can't edit without building Quest from the source code).

on ready is also a Quest script, which waits for other Quest scripts to finish before running whatever is in its block of code. Note that on ready does not wait for Quest functions, as it does not even know they exist. It only knows about Quest scripts, because they live in Script Town, too.


ShowMenu is a Quest function. It has a callback, which is the script you add when coding. ShowMenu doesn't know about anything else going on in the game except that it has a list of choices, and it will try to match the next command to those choices. If there is a match, it pass the choice to the script via the result parameter, then it runs the script.


on ready doesn't work together with ShowMenu, because one is a script and the other is a function. They work differently and separately.

on ready works with ask but not Ask for this same reason.

I think it also works with get input.


http://docs.textadventures.co.uk/quest/scripts/on_ready.html

http://docs.textadventures.co.uk/quest/blocks_and_scripts.html


Looks like your code is pretty well structured.

I think that putting the wait block at the end is problematic, because you don't ant that bit to happen if you chose 'Special'. You only want it to appear if you want the player to choose from those three options.

If I was you, I'd make a function which does the following:

  • If (monster is dead) {
    • remove the monster object
  • }
    else if (player is dead) {
    • show messages, game over, etc
  • }
    else {
    • call DoFight again
  • }

That's a logical group of things to do together as a function, as you'll want them to be done after each round whether you fought, healed, used a grenade, or something else.

Then you call that function:

  1. After fighting (within the if (result="Fight") block)
  2. After failing to escape
  3. In the DoSpecial function, after resolving the player's action.

That way, if you choose "Special", the fight menu isn't shown again until after you've chosen what you're doing for that round.

Additional advice: You could also make DoFight have fewer parameters. Make your 'fight' verb for the enemy look like:

<fight type="script">
  game.enemy = this
  DoFight()
</fight>

Within the DoFight, DoSpecial, and CheckEnemy functions, you can use game.enemy.combatskill, or game.enemy.name, or whatever. You don't need to create copies of all those variables every time you call the function.

Alternatively: I can see another way I'd arrange the code. But not sure if I'd recommend it or not.
I'll post in a moment in case you're interested.


@KV
If you want something that looks like on ready but waits for ShowMenu/Ask…

Edit: Made it work with Ask too; and fixed a weird edge case where a script called by OnReady calls it again and also calls Ask.

<function name="OnReady" parameters="callback">
  obj = GetObject("__waitformenu_turnscript")
  if (obj = null) {
    if (not HasScript (game, "menucallback") and not HasScript (game, "askcallback")) {
      invoke (callback)
      return ()
    }
    create turnscript ("__waitformenu_turnscript")
    obj = GetObject("__waitformenu_turnscript")
    obj.callbacks = NewList()
    obj.script => {
      scriptlist = this.callbacks
      while ((ListCount(scriptlist) > 0) and not HasScript (game, "menucallback") and not HasScript (game, "askcallback")) {
        callback = ListItem (scriptlist, 0)
        list remove (scriptlist, callback)
        invoke (callback)
      }
      if (ListCount (scriptlist) = 0) {
        this.enabled = false
      }
    }
  }
  list add (obj.callbacks, callback)
  obj.enabled = true
</function>

(multiple calls to OnReady will queue up the callbacks passed to them. If one of the callbacks creates a menu, subsequent ones will be put on hold)


Edit: silly error

Here's how I'd do it.
This is probably not the best way, but I like having stuff in one place.

  <type name="combatenc">
    <displayverbs type="listextend">Fight</displayverbs>
    <inventoryverbs type="listextend">Fight</inventoryverbs>
    <fight type="script">
      DoFight(this)
    </fight>
    <autocombat type="boolean">false</autocombat>
    <combatskill type="int">1</combatskill>
    <endurance type="int">1</endurance>
    <wpnbonus type="int">0</wpnbonus>
    <wpndamage type="int">1</wpndamage>
    <noescape type="boolean">true</noescape>
  </type>
  <function name="DoFight" parameters="enemy">
    game.enemy = enemy
    game.incombat = true
    DoCombatTurn("")
  </function>

  <function name="DoCombatTurn" parameters="action">
    <![CDATA[
    enemy = game.enemy
    UpdateEnemyStatus()
    ClearScreen
    menulist = Split ("Fight;Special;Escape")
    switch (action) {
      case ("Fight") {
        msg ("You fight the "+GetDisplayAlias(game.enemy)+"!")
        RollCombat()
      }
      case ("Special") {
        menulist = Split (ProcessText("{if game.allowgrenades:Grenade;}Heal;Cancel"))
      }
      case ("Escape") {
        if(GetBoolean(game.enemy, "noescape")) {
          msg ("You cannot escape from this combat.")
        }
        else {
          msg ("You escape from combat.")
          game.incombat = false
        }
      }
      case ("Grenade") {
        UseGrenade()
      }
      case ("Heal") {
        msg ("You can't do that now.")
      }
      case ("", "Cancel") {
        // The player has either cancelled out of the "Special" menu, or this is the first turn
        // so we just show the menu
      }
      default {
        // This will only happen if there is a typo in one of the command names
        // so that the "menulist" item and the case() statement don't match
        // This is a pain to debug if it happens, so it's always worth including this block just in case
        error ("CAN'T HAPPEN! In DoCombatTurn, menu result was: "+action)
      }
    }
    // I'm assuming here that "CheckPlayer" and "CheckEnemy" will set game.incombat to false
    // if either participant has died.
    if (GetBoolean (game, "incombat")) {
      CheckPlayer()
    }
    if (GetBoolean (game, "incombat")) {
      CheckEnemy()
    }
    if (GetBoolean (game, "incombat")) {
      // If this turn's action hasn't ended combat
      // ask them what to do next turn
      ShowMenu ("You are in combat with a "+GetDisplayAlias(enemy)+". What would you like to do?", menulist, false) {
        DoCombatTurn (result)
      }
    }
    ]]>
  </function>

Thanks again, everyone!

mrangel, I solved it thanks to your "make a separate function to check if combat should end" suggestions. So obvious I feel stupid! But by gettting rid of that after the main combat menu, I now have:

  <function name="DoFight" parameters="enemy,enemyname,enemycombatskill,enemyendurance,enemywpnbonus,enemywpndamage,enemyescape">
    <![CDATA[
    game.incombat = true
	game.enemy = enemy
    game.enemyname = enemyname
    game.enemycombatskill = enemycombatskill
    game.enemyendurance = enemyendurance
    game.enemywpnbonus = enemywpnbonus
    game.enemywpndamage = enemywpndamage
    game.noescape = enemyescape
	UpdateEnemyStatus()
	ClearScreen
	menulist = NewStringList()
	list add (menulist, "Fight")
	list add (menulist, "Special")
	list add (menulist, "Escape")
	ShowMenu ("You are in combat", menulist, false) {
	  if(result = "Fight") {
	    msg ("You fight the "+game.enemyname+"!")
		RollCombat()
		CheckPlayer()
		CheckEnemy()
		wait {
			CheckCombatEnd()
		}
	  }
	  else if(result = "Special") {
   		DoSpecial()
	  }
	  else if(result = "Escape") {
		if(GetBoolean(game, "noescape")) {
	    	msg ("You cannot escape from this combat.")
			wait {
				CheckCombatEnd()
			}
		}
		else {
	    	msg ("You escape from combat.")
	    	game.incombat = false
			wait {
				CheckCombatEnd()
			}
	    }
	  }
	}
    ]]>
  </function>

  <function name="DoSpecial">
    <![CDATA[
	spmenulist = NewStringList()
	if(GetBoolean(game, "allowgrenades")) {
		list add (spmenulist, "Grenade")
	}
	list add (spmenulist, "Heal")
	list add (spmenulist, "Cancel")
	ShowMenu ("What do you want to do?", spmenulist, true) {
	  if(result = "Grenade") {
		UseGrenade()
		CheckEnemy()
		wait {
			CheckCombatEnd()
		}
	  }
	  else if(result = "Heal") {
	    msg ("You can't do that now.")
		wait {
			CheckCombatEnd()
		}
	  }
	  else if(result = "Cancel") {
		CheckCombatEnd()
	  }
	}
    ]]>
  </function>

  <function name="CheckCombatEnd">
    <![CDATA[
	if ((game.enemyendurance > 0) and (GetBoolean(game, "incombat"))) {
		DoFight(game.enemy,game.enemyname,game.enemycombatskill,game.enemyendurance,game.enemywpnbonus,game.enemywpndamage,game.noescape)
	}
	else {
	  	if (game.enemyendurance < 1) {
			RemoveObject (game.enemy)		  
	  	}
	  	ClearEnemyStatus()
	}
    ]]>
  </function>

Which is a bit messy, but seems to work as I wanted (pauses after each "turn" but not mid-menu).

I will look at the idea of getting rid of the "copy the variables to global" since I agree it's a bit hacky and messy, it was the only way around ShowMenu not passing local variables I could think of but would prefer a better way to do it.

Will have to re-read the new posts again, but excited this now works!

I've been working on a "survival game" library for a few months privately, but combat was the thing I was hesitant to approach. As someone who's nostalgia is far more for gamebooks than text adventures (my main childhood gaming was graphic adventures like Space Quest and King's Quest), being able to do "Deververse" or "Fighting Fantasy" style combat is awesome!


EDIT

Sorry for the double post, but just want to add more thanks as I now have it working without needing to copy all the enemy stats, as per mrangel's suggestion (this also solved an umentioned issue I had with trying to pass them back to the enemy if the player escaped).

Really happy to have a working turn-based combat system now and have already enjoyed adding lots of tweaks; it now can be set to simulate three gamebook combat rulesets, has special moves (grenades, healing, etc.), initiative, a stealth system, equipment and multiple enemy fights.

I would love to share it as a library here for others to use if there's an interest once it's done. :)


Log in to post a reply.

Support

Forums