Polish 07 - Using Spells and Items Outside of Combat
This is the seventh article in a series that takes the RPG from the “How to Make an RPG” project and adds a little more polish to one element.
In the previous polish phase we added a spell-browser to the in-game menu. In this article we’ll make that menu interactive.
As you play an RPG you discover spells and items that can heal and revive your characters. In the example game, at the end of the book, curative items can only be used during combat. This means if your character dies during combat, the only way to revive them is to make it to a town and rest at an inn. That’s not good!
After combat a character may be:
- KO’ed
- Down on mana
- Down on HP
- Suffering from a status effect
At this point the player should be able to use spells and items to heal the party!
Here’s the party damaged after battle:

How will the party the recover?
Note: This polish article got a bit out of hand. It’s big, too detailed and adds a lot!
Todo
Before adding a feature it’s a good idea to scope out what’s needed.
- Some way to mark spells and items as “Can be used on the map”
- Selection code to chose the wounded character(s).
- Spell and item effects need to be shared between the combat and menu. (Otherwise we’re duplicating code and that it’s going to get out of sync)
- Make sure mana checks are applied at the menu level. Spells aren’t free :)
Decision Points
There’s one major decision to make about this feature - which maps do we want to allow the player to use items and spells? The world map, town maps - how about dungeons?
I’ve decided the player can use spells on any map.
Spell Mark Up
How are spells defined in our game? Here’s a definition for the Fire spell.
1["fire"] =2{3 name = "Fire",4 action = "element_spell",5 element = "fire",6 mp_cost = 8,7 base_damage = {3, 5},8 base_hit_chance = 1,9 time_points = 10,10 target =11 {12 selector = "WeakestEnemy",13 switch_sides = true,14 type = "One"15 },16 description = "Damages an enemy with elemental fire!"17},
We’re going to add a new optional field to the spell definitions.
This field is called can_use_on_map and determines if the spell can be used on the map. This field defaults to false. In the case of the fire spell, shown above, we can add can_use_on_map = false or just leave it to assume it’s default value.
In book we didn’t introduce any healing spells appropriate for the map, so let’s add one! All spells are defined in the SpellDB table in SpellDB.lua.
1["heal"] =2{3 name = "Heal",4 action = "hp_restore_spell",5 mp_cost = 8,6 base_heal = 100,7 base_hit_chance = 1,8 time_points = 10,9 can_use_on_map = true, -- <-- this is new!10 target =11 {12 selector = "MostHurtParty",13 switch_sides = true,14 type = "One"15 },16 description = "Heals some health of a single party member."17},
There are a lot of fields, so let me explain what they all do:
- name - The name shown in the menus.
- action - The name of the function that applies the spell effect. (We haven’t written this yet!)
- mp_cost - How much the spell costs to cast.
- base_hit_chance - A number 0 to 1 that determines the chance the spell hits. Here 1 means a 100% chance of hitting.
- time_points - The length of time the spell takes to cast in “points”. Using an item might be a single point, so even if used later than spell, it would take precedence and happen first.
- can_use_on_map - Our new field determining if we can use the spell on the map.
- target - table that determines which characters can be targeted.
Restore Action
The action field of the heal spell is set to hp_restore_spell. We haven’t written this yet, so let’s look at the existing hp_restore used by items. This is defined in CombatActions.lua.
1["hp_restore"] =2function(state, owner, targets, def)34 local restoreAmount = def.use.restore or 2505 local animEffect = gEntities.fx_restore_hp6 local restoreColor = Vector.Create(0, 1, 0, 1)78 for k, v in ipairs(targets) do910 local stats, character, entity = StatsCharEntity(state, v)1112 local maxHP = stats:Get("hp_max")13 local nowHP = stats:Get("hp_now")1415 if nowHP > 0 then16 AddTextNumberEffect(state, entity, restoreAmount, restoreColor)17 nowHP = math.min(maxHP, nowHP + restoreAmount)18 stats:Set("hp_now", nowHP)19 end2021 AddAnimEffect(state, entity, animEffect, 0.1)2223 end24end
A pretty straight forward action, it applies the restore_amount of the item to each character it targets.
It’s worth noting: the application of the action is directly tied to the combat state. It updates the health but also adds special effects. We need to break up the action into the a general part and a combat specific part. The alternative is to create two actions hp_restore_combat and hp_restore_map but it’s a bad idea to duplicate this code. It means whenever we update the action code, we have to do it in two places and this means, at some point - it’s going to get out of sync!
We’ll worry about this later, let’s add hp_restore_spell.
1["hp_restore_spell"] =2function(state, owner, targets, def)34 local restoreAmount = def.base_heal or 1005 restoreAmount = restoreAmount * owner.mLevel6 local animEffect = gEntities.fx_restore_hp7 local restoreColor = Vector.Create(0, 1, 0, 1)89 for k, v in ipairs(targets) do1011 local stats, character, entity = StatsCharEntity(state, v)1213 local maxHP = stats:Get("hp_max")14 local nowHP = stats:Get("hp_now")1516 if nowHP > 0 then17 AddTextNumberEffect(state, entity, restoreAmount, restoreColor)18 nowHP = math.min(maxHP, nowHP + restoreAmount)19 stats:Set("hp_now", nowHP)20 end2122 AddAnimEffect(state, entity, animEffect, 0.1)2324 end25end
We now have a hp_restore and a hp_restore_spell, these seem similar but they’re not. Items are generally consistent, they do the same thing again and again. Spells are less predictable, depend on the caster and have a certain randomness, so it’s makes sense to seperate these actions.
The hp_restore_spell has a crude formula that heals 100 points of HP per the level of the caster. This can be refined later but it’s good enough for our purposes!
Item Mark Up
Items are defined in a similar way to the spells. Here’s an entry from the ItemDB.
1{2 name = "Heal Potion",3 type = "useable",4 description = "Heal a small amount of HP.",5 use =6 {7 action = "hp_restore",8 restore = 250,9 target =10 {11 selector = "MostHurtParty",12 switch_sides = true,13 type = "One"14 },15 hint = "Choose target to heal.",16 can_use_on_map = true,17 },18 price = 50,19},
We’ve added the can_use_on_map field to the use table and set it to true. We’ll also do this to the following items:
- Life Salve
- Mana Potion
That’s the markup done!
The Heal Spell
The Heal spell needs to be usable in combat before we get it working on the world map. For testing, let’s add the spell to the mage’s repertoire. Below you can see the spell inserted into to the mage definition, in the gPartyMemberDefs table.
1gPartyMemberDefs =2{3 -- code omitted45 mage =6 {7 -- code omitted89 actionGrowth =10 {11 [1] =12 {13 ['magic'] = { 'bolt', 'heal' },14 },
Heal is a level one spell. Start a new game (it won’t automatically be awarded from a save game), enter combat and you can try it out!
Revisiting the Magic Menu
The player needs to know which spells can be cast on the world map. We’ll grey out any spell names that cannot be cast such as “Fire”. Spell names are drawn by a function called RenderSpell. Here’s the updated function:
1function MagicMenuState:CanCast(spellDef)2 if spellDef == nil or not spellDef.can_use_on_map then3 return false4 end56 return self.mCharacter:CanCast(spellDef)7end89function MagicMenuState:RenderSpell(menu, renderer, x, y, item)1011 local font = gGame.Font.default1213 if item == nil then14 font:DrawText2d(renderer, x, y, "--")15 else16 local spell = SpellDB[item]17 local horzSpace = 9618 local color = Vector.Create(107/255, 107/255, 107/255, 1)1920 if self:CanCast(spell) then21 color = Vector.Create(1, 1, 1, 1)22 end2324 font:DrawText2d(renderer, x, y, spell.name, color)25 font:DrawText2d(renderer, x + horzSpace, y,26 string.format("%d", spell.mp_cost), color)27 end28end
There are two tests to see if a spell name should appear in white:
- Does it have a true
can_use_on_mapflag? - Does the mage have enough mana to cast the spell?
I added a new CanCast function to the Actor class, here’s the implementation in Actor.lua.
1function Actor:CanCast(spellDef)2 return spellDef.mp_cost <= self.mStats:Get("mp_now")3end
The CombatState has also been updated to use this function. The CanCast test is better in the actor because later, if we want add a “silence” status effect, we can easily do it here and have it apply everywhere.
Selecting Targets in the Character Menu
Let’s have quick recap of what needs doing:
- Enter a selection state from the item and spell menu
- Upgrade the selector so we can select the entire party at once
- Work out how we handle the excution of spells or items from the menu
On choosing a spell to cast, we’ll switch to the FrontMenuState so the player can select the target(s). We’ll base the selection code on the CombatSelector from the CombatTargetState (in fact to tidy up the code a little, I’ve moved CombatSelector into it’s own file CombatSelector.lua).
Let’s create a new file called MenuActorSelector.lua and copy in the code below.
1MenuActorSelector =2{3 FirstPartyMember = function(menu)4 end,56 FirstMagicUser = function(menu)7 end,89 MostHurtMember = function(menu)10 end,1112 MostDrainedParty = function(menu) -- lowest mp13 end,1415 DeadParty = function(menu) -- first dead party member16 end,17}
These are the kind of selections we want to be able to make. The names are chosen to match those in the CombatSelector.
Let’s make the FrontMenuState use the selector code and then we’ll fill in all the selection types we need. We’ll make item menu work first, as that’s simpler than the magic menu.
When we choose an item that can be used we want move to the FrontMenuState, then push a MenuTargetState on the top of the stack. The menu target state will control which targets are selected. In the MenuTargetState the player can then either:
- Chose a valid party member
- Cancel the selection
Whichever choice the player makes they will be returned to the item menu. This means the MenuTargetState needs to store which state it was launched from.
Preparing FrontMenuState
By making some small additions to the FrontMenuState we’ll able to control the party selection more easily. Let’s begin with a very simple function HideOptionsCursor, that hides the cursor. When we’re in selection mode, we don’t want any other cusors on screen confusing things. Here’s the code:
1function FrontMenuState:HideOptionsCursor()2 self.mSelections:HideCursor()3end
To select characters we need to know where to render the selection marker and which party members can be selected. Next we’ll add a function to make this easier.
1function FrontMenuState:GetPartyAsSelectionTargets()2 local targets = {}34 local x = self.mPartyMenu.mX5 local y = self.mPartyMenu.mY6 local cursorWidth = self.mPartyMenu:CursorWidth()78 for k, v in ipairs(self.mPartyMenu.mDataSource) do910 local indexFrom0 = k - 11112 table.insert(targets,13 {14 x = x + cursorWidth * 0.5,15 y = y - (indexFrom0 * self.mPartyMenu.mSpacingY),16 summary = v17 })18 end19 return targets20end
GetPartyAsSelectionTargets returns a list of the character summaries for each member of the party, it also returns an X, Y position so we know where to place the cursor(s).
Finally we need to move some code. In FrontMenuState:Render there are two lines of code:
1function FrontMenuState:Render(renderer)2 -- code omitted34 local partyX = self.mLayout:Left("party") - 165 local partyY = self.mLayout:Top("party") - 456 self.mPartyMenu:SetPosition(partyX, partyY)7 self.mPartyMenu:Render(renderer)8end
The partyX and partyY variables define where the summaries are displayed in the FrontMenuState. We need to know the value of these variables when the FrontMenuState is first created. Therefore we’ll cut them out of the Render function and paste them at the bottom of the Enter function. You can see their new home below.
1function FrontMenuState:Enter()2 local partyX = self.mLayout:Left("party") - 163 local partyY = self.mLayout:Top("party") - 454 self.mPartyMenu:SetPosition(partyX, partyY)5end
Ok, on to new code!
Menu Target State
MenuTargetState doesn’t exist so let’s create it.
1MenuTargetType =2{3 One = "One",4 All = "All",5}67MenuTargetState = {}8MenuTargetState.__index = MenuTargetState9function MenuTargetState:Create()10 local this = {}1112 setmetatable(this, self)13 return this14end1516function MenuTargetState:Enter()17end1819function MenuTargetState:Exit()20end2122function MenuTargetState:Update(dt)23end2425function MenuTargetState:Render(renderer)26end
As with the MenuActorSelector, it’s just a skeleton for now, we’ll fill in the details as they’re needed. Let’s start with the following flow:
1[ItemMenuState] --use item--> [MenuTargetSelector]2[MenuTargetSelector] --cancel--> [ItemMenuState]
When the player choses an item, we need to know about it. Therefore let’s add a callback when an item is clicked. In the ItemMenuState, items are stored in a Selection menu, you can see the code below.
1Selection:Create2{3 data = gGame.World.mItems,4 spacingX = 256,5 columns = 2,6 displayRows = 8,7 spacingY = 28,8 rows = 20,9 RenderItem = function(self, renderer, x, y, item)10 gGame.World:DrawItem(self, renderer, x, y, item)11 end12},
We need to add the OnSelection callback to this.
1Selection:Create2{3 -- code omitted45 RenderItem = function(self, renderer, x, y, item)6 gGame.World:DrawItem(self, renderer, x, y, item)7 end,8 OnSelection = function(...) this:OnUseItem(...) end, <-- new9},
Then let’s create the new OnUseItem function.
1function ItemMenuState:CanUseItem(itemDef)2 local useDef = itemDef.use or {}3 return useDef.can_use_on_map == true4end56function ItemMenuState:OnUseItem(index, item)7 local itemDef = ItemDB[item.id]8 if not self:CanUseItem(itemDef) then9 return10 end1112 local targetState = MenuTargetState:Create13 {14 originState = self,15 stack = gGame.Stack,16 stateMachine = self.mStateMachine,17 targetType = MenuTargetType.One,18 selector = MenuActorSelector["FirstMagicUser"],19 OnCancel = function(target) print("Cancelled") end,20 OnSelect = function(target) print("Selected", target) end21 }22 gGame.Stack:Push(targetState)23end
In OnUseItem we create the target state with all the parameters it needs and push it on top of the global stack.
There and Back Again
If you run the code now and select an item, the game freezes because we’ve pushed an empty state on top of the stack! Let’s update the code so we’re taken to the FrontStateMenu, then we’ll add an ability to cancel and return to the ItemMenuState, once those work we can dig into the selector more deeply.
Here’s a updated MenuTargetState.
1MenuTargetType =2{3 One = "One",4 All = "All",5}67MenuTargetState = {}8MenuTargetState.__index = MenuTargetState9function MenuTargetState:Create(params)1011 params = params or {}1213 local this =14 {15 mStack = params.stack,16 mStateMachine = params.stateMachine,17 mOriginalState = params.originState,18 mStartedInFrontMenu = (params.originId == "frontmenu"),1920 mTargetType = params.targetType or MenuTargetType.One,21 mSelector = params.selector or MenuActorSelector["FirstPartyMember"],2223 mFrontMenuState = nil,24 mMarker = Sprite.Create(),2526 mTargets = nil,27 mActiveTargets = nil,2829 OnSelect = params.OnSelect or function() end,30 OnCancel = params.OnCancel or function() end,31 }3233 local markTexture = Texture.Find('cursor.png')34 this.mMarkerWidth = markTexture:GetWidth()35 this.mMarker:SetTexture(markTexture)3637 setmetatable(this, self)38 return this39end4041function MenuTargetState:Enter()4243 -- Selection for previous state should be ignored!44 self.mIgnoreSpaceRelease = Keyboard.JustPressed(KEY_SPACE)4546 if not self.mStartedInFrontMenu then47 self.mStateMachine:Change("frontmenu")48 end4950 self.mFrontMenuState = self.mStateMachine:Current()51 self.mFrontMenuState:HideOptionsCursor()5253 -- Need to get the list of targets with position54 -- Of the form { x = 0, y = 0, summary = Y }55 self.mTargets = self.mFrontMenuState:GetPartyAsSelectionTargets()5657 -- Filter the target by the selector58 self.mActiveTargets = self.mSelector(self.mTargets)5960 -- If the selector is of type one then we need to select a default target61end6263function MenuTargetState:Exit()64end6566function MenuTargetState:Update(dt)67end6869function MenuTargetState:Render(renderer)70 for k, v in ipairs(self.mActiveTargets) do71 self.mMarker:SetPosition(v.x, v.y)72 renderer:DrawSprite(self.mMarker)73 end74end7576function MenuTargetState:HandleInput()7778 -- Released otherwise the keypress gets picked up in the state79 -- we transition to80 if Keyboard.JustReleased(KEY_BACKSPACE) or81 Keyboard.JustReleased(KEY_ESCAPE) then82 self.OnCancel()83 self:Back()84 end8586 if Keyboard.JustReleased(KEY_SPACE) then8788 if self.mIgnoreSpaceRelease then89 self.mIgnoreSpaceRelease = false90 return91 end9293 self.OnSelect(self.mActiveTargets)94 self:Back()95 end96end9798-- Back out of targetting99function MenuTargetState:Back()100 self.mStack:Pop() -- remove the targeting state101102 -- If we're not already in the front menu, then restore previous menu103 if not self.mStartedInFrontMenu then104 self.mStateMachine.mCurrent = self.mOriginalState105 end106end
A bit of a code dump but there’s nothing too tricky going on here.
Well actually, some of the input handling is a little tricky: if you switch between states, a keypress can registered in both states, in the same frame which is annoying! I’ve added code to get around that but really it should be handled more gracefully.
Also note: When we return to the previous state, we don’t call Change. We want to skip all the Enter/Exit stuff and just return to the state as it was.
On entering the target state we get the list of all party members using GetPartyAsSelectionTargets. We filter the list using the mSelector. The selector is defined by the spell or item in use. A revive spell selector should only let you target KO’ed members for instance. (What if there are no KO’ed members? Well, that’s an edge case we handle poorly!)
The MenuTargetState draws a selector over each active target. When the player presses Space it calls it’s selection callback with the active target list. When they press Backspace it returns to the ItemMenuState. Try it out, it works pretty well.
There’s no code to handle targeting types yet, if we can select only one target, or if we can select multiple. That kind of thing. If there’s only one target that can be selected we need to be choose which one and so on.
Selection Types : Everybody
Let’s deal with the easiest selection type first - the entire party. For testing purposes we’ll add an new item the “Mega Heal Potion”. Here’s the def to add to the bottom of the ItemsDB table.
1{2 name = "Mega Heal Potion",3 type = "useable",4 description = "Heal all party members for a small amount of HP.",5 use =6 {7 action = "hp_restore",8 restore = 250,9 target =10 {11 selector = "SideParty",12 switch_sides = true,13 type = "Side"14 },15 hint = "Choose target to heal.",16 can_use_on_map = true17 },18 price = 250,19},
It’s similar to the normal “Heal Potion” but
- More expensive
- Target is of type
Siderather thanOne. - Selector is equal to
SidePartyinstead ofMostHurtParty.
We haven’t defined the combat selector for SideParty so let’s add it now.
1CombatSelector =2{3 -- code omitted4 SideParty = function(state)5 return state.mActors["party"]6 end,7}
Now we can add a Mega Heal Potion to the party inventory and use it in combat.
Next let’s add the code to call when the player selects the target(s). To make this happen let’s update the OnUseItem function in the ItemMenuState.
1function ItemMenuState:OnUseItem(index, item)2 local itemDef = ItemDB[item.id]3 if not self:CanUseItem(itemDef) then4 return5 end67 local selectId = itemDef.use.target.selector89 local targetState = MenuTargetState:Create10 {11 originId = "items",12 stack = gGame.Stack,13 stateMachine = self.mStateMachine,14 targetType = itemDef.use.target.type,15 selector = MenuActorSelector[selectId],16 OnCancel = function(target) print("Cancelled") end,17 OnSelect = function(target) print("Selected", target) end18 }19 gGame.Stack:Push(targetState)20end
The targetType and selector fields are now set by the item.
In MenuActorSelector let’s add a selector called SideParty too.
1MenuActorSelector =2{3 -- code omitted4 SideParty = function(targets)5 return targets -- return everyone!6 end7}
Trying out the Mega Potion
To make testing this easier, let’s add a debug button in the main.lua file.
1function update()23 -- code omitted45 if Keyboard.JustPressed(KEY_D) then6 for k, v in pairs(gGame.World.mParty.mMembers) do7 local stats = v.mStats8 stats:Set("hp_now", stats:Get("hp_now")*0.5)9 end10 local megaHealPotionId = 1611 gGame.World:AddItem(megaHealPotionId, 1)12 end
If we run the game and press D, it halves the health of all the party members and gives the player a mega potion.
Run the game now, press D, choose the mega potion and by default every member of the party will be targeted.
Hooking up Item Use
When a players choses the targets we get a callback to OnSelect. At the moment, OnSelect just prints a message. Let’s make it call OnItemTargetsSelected.
1OnSelect = function(targets) self:OnItemTargetsSelected(itemDef, targets) end
And now let’s write that function.
1function ItemMenuState:OnItemTargetsSelected(itemDef, targets)2 local action = itemDef.use.action3 CombatActions[action](self,4 nil, -- the person using the item5 targets,6 itemDef,7 "item")89 -- Remove the item from the inventory10 gGame.World:RemoveItem(itemDef.id)11end
The last parameter in the CombatActions indicates that we’re calling this action from the “item” menu.
Here’s the current hp_restore combat action we reference in the heal potion def.
1["hp_restore"] =2function(state, owner, targets, def)3 local restoreAmount = def.use.restore or 2504 local animEffect = gEntities.fx_restore_hp5 local restoreColor = Vector.Create(0, 1, 0, 1)67 for k, v in ipairs(targets) do89 local stats, character, entity = StatsCharEntity(state, v)1011 local maxHP = stats:Get("hp_max")12 local nowHP = stats:Get("hp_now")1314 if nowHP > 0 then15 AddTextNumberEffect(state, entity, restoreAmount, restoreColor)16 nowHP = math.min(maxHP, nowHP + restoreAmount)17 stats:Set("hp_now", nowHP)18 end1920 AddAnimEffect(state, entity, animEffect, 0.1)2122 end2324end,
We need to update this action, so it works on the menu as well. We must seperate the “behind-the-scenes” logic and foreground special effects.
This means it’s going to get longer!
1local function StatsForCombatState(targets)2 local statList = {}3 for k, v in ipairs(targets) do4 table.insert(statList, v.mStats)5 end6 return statList7end89local function StatsForMenuState(targets)10 local statList = {}11 for k, v in pairs(targets) do12 local stats = v.summary.mActor.mStats13 table.insert(statList, stats)14 end15 return statList16end1718CombatActions =19{20 ["hp_restore"] =21 function(state, owner, targets, def, stateId)2223 local extractStatFunction = StatsForCombatState24 if stateId == "item" then25 extractStatFunction = StatsForMenuState26 end2728 local statList = extractStatFunction(targets)29 local restoreAmount = def.use.restore or 25030 for k, v in ipairs(statList) do31 local maxHP = v:Get("hp_max")32 local nowHP = v:Get("hp_now")33 if nowHP > 0 then34 nowHP = math.min(maxHP, nowHP + restoreAmount)35 v:Set("hp_now", nowHP)36 end37 end3839 if stateId == "item" then40 return41 end4243 --44 -- Combat Effects45 --4647 local animEffect = gEntities.fx_restore_hp48 local restoreColor = Vector.Create(0, 1, 0, 1)4950 for k, v in ipairs(targets) do5152 local stats, character, entity = StatsCharEntity(state, v)5354 if stats:Get("hp_now") > 0 then55 AddTextNumberEffect(state, entity, restoreAmount, restoreColor)56 end5758 AddAnimEffect(state, entity, animEffect, 0.1)59 end6061 end,
Feels like this code could tighter but it works for now.
You can run this code and it works. The mega-heal potion can be used in both combat and the menus. When used from the item menu, the effect of the item isn’t well communicated but that can be a future polish article!
Selection Types : One
Items that target all the party work, now let’s make single target items work too.
We don’t need to add a new item to test this, we already have three!
- Heal Potion
- Life Salve
- Mana Potion
The heal potion shares the same action as the “Mega Heal Potion”, so that will work. Life Salve and the Mana Potion need their actions updating. These updates are pretty lengthy so you can check them out over here. And we also need to update the selectors they use which can be seen here.
Moving the Selector
When choosing the target for an item, the MenuTargetState is on the top of the stack. Let’s update the HandleInput function to detect up and down keypresses and react to them.
1function MenuTargetState:HandleInput()23 -- code omitted45 if Keyboard.JustPressed(KEY_UP) then6 self:Up()7 elseif Keyboard.JustPressed(KEY_DOWN) then8 self:Down()9 end10end
When selecting a single target we need to keep track of who’s selected. We’ll do that with an index value.
1function MenuTargetState:FindIndex()23 for k, v in ipairs(self.mTargets) do4 if self.mActiveTargets[1] == v then5 return k6 end7 end89 print("Error: Failed to find index!")10 return 11112end1314function MenuTargetState:Enter()1516 -- code omitted1718 if self.mTargetType == MenuTargetType.One then19 self.mIndex = self:FindIndex()20 end21end
When we enter the MenuTargetState and we’re only selecting one party member we need to find the index of the default party member. Then we can use this index to let the player move around the party menu.
Using the index let’s implement the Up, Down functions and let the player choose who to target.
1function MenuTargetState:Up()2 if self.mTargetType ~= MenuTargetType.One then3 return4 end56 local newIndex = self.mIndex78 while newIndex > 0 do9 newIndex = newIndex - 11011 local targets = self.mSelector({self.mTargets[newIndex]})1213 if next(targets) then14 self.mIndex = newIndex15 self.mActiveTargets = targets16 return17 end18 end19end2021function MenuTargetState:Down()22 if self.mTargetType ~= MenuTargetType.One then23 return24 end2526 local newIndex = self.mIndex2728 while newIndex <= #self.mTargets do29 newIndex = newIndex + 13031 local targets = self.mSelector({self.mTargets[newIndex]})3233 if next(targets) then34 self.mIndex = newIndex35 self.mActiveTargets = targets36 return37 end38 end39end
When we press Down we move the current index down from the current index until we find a new index that the selector accepts. Up works in the same way.
Using Magic Spells from the Menu
Items can now be used from the in-game menu. Let’s make the same true of the spells.
The selection code for the spells lives in the MagicMenuState in the mSpellMenu. First thing, let’s add a new selection callback. Here’s the updated mSpellMenu below.
1self.mSpellMenu = Selection:Create2{3 data = character.mMagic,4 OnSelection = function(...) self:OnCastSpell(...) end, -- new!5 spacingX = 256,6 displayRows = 6,7 spacingY = 28,8 columns = 2,9 rows = 20,10 RenderItem = function(...) self:RenderSpell(...) end11}
The OnCastSpell function is called when the player chooses a spell from the menu. This includes greyed out spells, so we need to be careful to not cast those.
1function MagicMenuState:OnCastSpell(index, spellId)23 local spellDef = SpellDB[spellId]45 if self:CanCast(spellDef) then6 local selectId = spellDef.target.selector7 local targetState = MenuTargetState:Create8 {9 originState = self,10 stack = gGame.Stack,11 stateMachine = self.mStateMachine,12 targetType = spellDef.target.type,13 selector = MenuActorSelector[selectId],14 OnCancel = function(target) print("Cancelled") end,15 OnSelect = function(targets) self:OnSpellTargetsSelected(spellDef, targets) end16 }17 gGame.Stack:Push(targetState)1819 end20end
When a spell selects a target it calls OnSpellTargetsSelected, let’s write that next.
1function MagicMenuState:OnSpellTargetsSelected(targets)2 self.mCharacter:ReduceManaForSpell(spellDef)3 local action = spellDef.action4 CombatActions[action](self.mState,5 self.mCharacter,6 targets,7 spellDef,8 "magic_menu")9end
We don’t test if we can cast the spell because we already did that before entering the MenuTargetState.
We add a new function called ReduceManaForSpell check it out below:
1function Actor:ReduceManaForSpell(spellDef)2 local mp = self.mStats:Get("mp_now")3 local cost = spellDef.mp_cost4 local mp = math.max(mp - cost, 0)5 self.mStats:Set("mp_now", mp)6end
I’ve also updated CECastSpell to use this function too.
Finally we need to update the hp_restore_spell action, so it doesn’t try and add combat spell effects to the menu. Here’s the updated version in CombatActions.lua.
1["hp_restore_spell"] =2function(state, owner, targets, def, stateId)34 local extractStatFunction = StatsForCombatState5 if stateId == "magic_menu" then6 extractStatFunction = StatsForMenuState7 end89 local restoreAmount = def.base_heal or 10010 restoreAmount = restoreAmount * owner.mLevel11 local statList = extractStatFunction(targets)1213 for k, v in ipairs(statList) do14 local maxHP = v:Get("hp_max")15 local nowHP = v:Get("hp_now")16 if nowHP > 0 then17 nowHP = math.min(maxHP, nowHP + restoreAmount)18 v:Set("hp_now", nowHP)19 end20 end2122 if stateId == "magic_menu" then23 return24 end2526 local animEffect = gEntities.fx_restore_hp27 local restoreColor = Vector.Create(0, 1, 0, 1)2829 for k, v in ipairs(targets) do3031 local stats, character, entity = StatsCharEntity(state, v)3233 local nowHP = stats:Get("hp_now")3435 if nowHP > 0 then36 AddTextNumberEffect(state, entity, restoreAmount, restoreColor)37 end3839 AddAnimEffect(state, entity, animEffect, 0.1)40 end41end
I’m not happy with how fat these combat action functions are but they do the job until I can think of something nicer!
That’s all we need to do. We can now cast spells and use items from the in-game menu. This change was surprisingly big but definitely makes the game more playable!
Here’s a screenshot showing all characters being selected:

A little clean-up
The magic menu state uses the variable mCharacter, I’ve renamed that to mActor as that it references the Actor class not the Character class!
Summing Up
This has been a pretty big feature! It’s functional but it’s not very polished. A number of future articles will make using items on the map nicer for the user.
Here are few things that need addressing:
- There are now two ways to select party members in the front menu - there should only be one!
- The player doesn’t get enough feedback about the effects of items and spells on their targets
- When selecting a target there should be tool tip saying “Choose target” or similar.
- Perhaps lock off items and spells that don’t have a valid target (using the life-salve when no one is dead).
Source
There’s a github with an updated project here.