The gist of it is that the spell icon defined in the .SPL header is the one used in the spell book. The icon that the engine uses in the actionbar is calculated on the fly based on the ability header that is being used. Technically you can have the icon of a spell change depending on the character's level, etc., but I've never seen that used, even by modders.
So, how do I get the ability icon instead of the spell icon? ... it's complicated, very complicated. And of course the engine doesn't have a function defined for the purpose; instead it inlined that functionality into CGameSprite::GetQuickButtons() -_-
I'll have the name / description stuff up once I figure out how to calculate the correct quick-spell icon...
Technically you can have the icon of a spell change depending on the character's level, etc., but I've never seen that used, even by modders.
I think there were some priest spontaneous spell-conversion mods that tried this, setting the Cure/Cause wounds at levels 51/101, along with their icons, but ultimately it wasn't worthwhile since it was more useful to see the icon of the spell you were sacrificing.
Edit - even the game doesn't always generate the correct icon for spells in the actionbar, as it will default to the previous/next spell's icon if the spell doesn't have an ability less than or equal to the casters level.
hey @Bubb, do you know if it would be possible to use hotkey script you wrote and extend it to cast spells that have two phase casting mechanics. For instance spell immunity > spell immunity divination or someone who plays Wild Mage a lot... NRD > spell.
If all else fails, you should be able to do that using a combination of loops, actionBarTooltip[idx] and buttonArray:OnLButtonPressed(idx) (it's what I use for my 'extra slot') but it's honestly pretty gross and there's probably better ways :p
EEex_GetMemorizedInnateSpells() and the variants for the other spell types can help you grab this info. Though, as I was testing it myself, I found that both the memorization functions and many of the bits utility functions were broken
Thanks boss, works exactly as I had hoped.
(...although the names are hardcoded because I can't figure out how to translate resref => name,description x_x)
Also, strangely, the icons that are shown in spell.icon is not the same as the icons used in the actionBar(?). I'm having to change "XXXXXXXC" to "XXXXXXXB", although I think this may be a problem with the base game? Hardiness uses "SPCL907C.bam" as its spell icon but that file doesn't exist (causing NearInfinity to say 'no spell icon', even) so I have to change the ending C=>B. For comparison purposes, I also have to do that - comparing spell.icon to buttonArray:GetButtonBam(x), since the buttonArray uses B-type icons (wow what a thoroughly bug-free and error-proof way of activating skills).
I would love to know how, in terrible detail, you did this @Serity . I don't mind gross as long as the gross is behind the scenes (heh, you should see my code... ).
Edit: Also, I don't mind if you pretend I'm dumb. In fact, I prefer it, as I only understood some embarrassing fraction of what you just said :P
I would love to know how, in terrible detail, you did this @Serity . I don't mind gross as long as the gross is behind the scenes (heh, you should see my code... ).
Edit: Also, I don't mind if you pretend I'm dumb. In fact, I prefer it, as I only understood some embarrassing fraction of what you just said :P
Well, as long as the detail is terrible, then, sure. Because I'm terrible at detail. The bit about XXXC => XXXB is because in every spellfile, there is a reference to an image which is usually <spellID>C.BAM. For the action bar it needs to be changed to <spellID>B.BAM because not every skill has a C.BAM.
The spell-list is hard-coded with a priority order (if you don't have earlier spells, it checks later spells) because I couldn't think of a good organic method to set them in-game and because it's pretty much just for my (and friends') own personal use. Adjust that somehow if you want (there are a few ways you could do that but I'm not sure how to save it to your player data - you could save it to INI like 'Extra Slot Skill','Charactername','SPCL412', I guess).
Here's the code I used for that. I just wrote up the comments on the fly (because I'm a terrible no-commenter) so some things may be a little weird description-wise but it should be understandable. https://pastebin.com/1tM08UHa
Notes: It uses Lefreut's "spell description" pane for the actionAlt (RMB) with a change: "Infinity_FetchString(currentSpell.name/.description)" labels (in the SPELL_DESCRIPTION menu) are switched out for getFetchOrDefault(currentSpell.name/.description), which is just this:
function getFetchOrDefault(input)
local txt = Infinity_FetchString(input)
if (#txt == 0) then txt = input end
return txt
end
.. so that it looks up a string resref, but if it finds no result (for example if you throw "Hello world" at the function instead of 28000), it will just return the text you sent it instead of nothing.
If you don't use Lefreut's thennnn.. uh. I dunno. Comment out the actionAlt I guess. Or figure out how to make it work with your UI :P
You could expand this if you want - for example, make the 'extra slot' actually pop out into several "favorited spells" which has the UI work the same way, just take the first listed spell found, second, third..
Nice to see people working on actionbar stuff - the more code for reference the better I'm looking for a way to exit currently opened actionbar submenu (spell selection). There is no built-in buttonArray:OnCancelButtonClick() functionality and I can't exit via buttonArray:OnLButtonPressed without selecting something (also tried using values outside actionbar range). Any other ideas?
@swit: Yeah, the actionbar stuff is exciting. Honestly didn't think there was enough dedicated functions in EEex yet to make it graceful. Still could use some more functionality to make things less hacky, (I'm actually working on this right now), but still awesome to see.
The hacky, (yet vanilla friendly!), way to close the submenu would be:
Infinity_PressKeyboardButton('escape')
What I'm currently using in my local WIP EEex version is:
local g_pBaldurChitin = EEex_ReadDword(EEex_Label("g_pBaldurChitin"))
local m_pObjectGame = EEex_ReadDword(g_pBaldurChitin + EEex_Label("CBaldurChitin::m_pObjectGame"))
local m_cButtonArray = m_pObjectGame + 0x2654
local m_nLastState = EEex_ReadDword(m_cButtonArray + 0x1478)
EEex_Call(EEex_Label("CInfButtonArray::SetState"), {m_nLastState}, m_cButtonArray, 0x0)
This reverts the actionbar to the previous state. (It'll become its own function eventually)
The hacky, (yet vanilla friendly!), way to close the submenu would be:
Infinity_PressKeyboardButton('escape')
Yeah, that's what I used too before checking uses-remaining-count to avoid having to back out of the menu (I noted it in the Lua pastebin above as a comment). You can also just re-select the current unit but that's even more laaame.
My own personal code also includes a cooldown meter for the extra slot (just CastTimer) based on your work but I didn't include that above since including that's stretching it a bit more out of scope and into its own thing (I just used Infinity_SetArea on fill elements as I mentioned before).
As a cute little addition to Thievery, I made it so if you RMB the Thieving button, it uses Pick Pockets on the nearest nearby neutral NPC (and it displays what you stole in the chatlog too, although that's extremely hacky as well and not included in the spoiler - scan inventory when toggling Thieving [sequence 26/28] or RMBing, then upon catching "Pick Pockets Succeeded" in chat log, scan inventory again and print the difference).
function actionBarRMB(index)
local button = EEex_GetActionbarButton(index)
if (button == EEex_ACTIONBAR_TYPE.THIEVING) then -- == 12
EEex_LuaObject = EEex_GetActorIDSelected()
C:Eval("ActionOverride(EEex_LuaObject,PickPockets([NEUTRAL]))")
return
end
buttonArray:OnRButtonPressed(index)
end
--...
button { ... actionBar 1 ... actionAlt "actionBarRMB(1)" ... }
Sadly, it doesn't work unless you're in single-player or the host since C:Eval only works for the host =_= But this can be gotten around with some.. uh. Disgusting chat-box parsing/manipulation I guess? Have RMB send a chat message, have code watch the chatbox/combatlog for "!pickpockets actorId", remove the text, and if you're the host, execute the above code. Gross.
edit: How the heck do Infinity_[ClickObjectInWorld/ClickWorldAt/ClickScreen]() work? Nothing I'm putting in seems to work (actor IDs, x/y, etc) and just clicks at 0,0. If that worked I could maybe have it do [ThieveryLClick] -> Click nearby target..
edit: How the heck do Infinity_[ClickObjectInWorld/ClickWorldAt/ClickScreen]() work? Nothing I'm putting in seems to work (actor IDs, x/y, etc) and just clicks at 0,0. If that worked I could maybe have it do [ThieveryLClick] -> Click nearby target..
Infinity_ClickObjectInWorld() takes a script name, (as in what is defined at offset 0x280 of the .CRE), as its only param. Contrary to the function name, it doesn't click the object, but the ground at its feet. Used like so to click the ground near Minsc:
Infinity_ClickObjectInWorld("Minsc")
Infinity_ClickWorldAt() takes x and y as parameters. Is supposed to click ground using the x, y as relative coordnates to the viewscreen, but appears entirely broken. It seems to always click the top-left of the current viewscreen.
Infinity_ClickScreen() takes no parameters and clicks the center of the viewscreen.
Infinity_ClickItem() takes a menuItem userdata type and clicks the center of its area. Used like this:
Infinity_ClickItem(nameToItem["whaterNameHere"])
Note that this doesn't play button animations; it plays the click sound and does the action.
Infinity_ClickWorldAt() takes x and y as parameters. Is supposed to click ground using the x, y as relative coordnates to the viewscreen, but appears entirely broken. It seems to always click the top-left of the current viewscreen.
Infinity_ClickScreen() takes no parameters and clicks the center of the viewscreen.
Okay, turns out that ClickWorldAt does work properly as-expected, as does ClickScreen. The catch is that if your mouse cursor is not in the world (such as on the ActionBar or on a SideBar), it clicks on world 0,0(-ish), which means they're utterly useless for clickable interface buttons.
...not.
Infinity_HoverMouseOver(x,y) "moves" where your mouse cursor is in the game world. It doesn't ACTUALLY move it, but the game engine thinks it does. So if you combine them like so, you can force a click in the game world:
B3Neutral = EEex_ParseObjectString("[NEUTRAL]")
function testFunc()
local actorID = EEex_EvalObjectAsActor(B3Neutral, EEex_GetActorIDSelected())
local x,y = EEex_GetActorLocation(actorID)
Infinity_HoverMouseOver(x,y)
Infinity_ClickWorldAt(x,y)
end
..moves you to the nearest Neutral. Yes, moves, not clicks. ..however, once you're actually there, or if you hit that function twice rapidly, it will click them! This is because Infinity_HoverMouseOver() will instantly move your mouse "into the game world", but it won't move to the proper x,y for one frame (I think?). If you don't include HoverMouseOver before ClickWorldAt, it'll just click at 0,0 in the game world (assuming your cursor is over the interface).
..so, you can fix that up to insta-click on an NPC with something like..
B3Neutral = EEex_ParseObjectString("[NEUTRAL]")
doTestClick = nil
function testFunc()
local actorID = EEex_EvalObjectAsActor(B3Neutral, EEex_GetActorIDSelected())
local x,y = EEex_GetActorLocation(actorID)
Infinity_HoverMouseOver(x,y)
doTestClick = {x,y}
end
function testFuncRepeater()
if (doTestClick) then
Infinity_ClickWorldAt(doTestClick[1],doTestClick[2])
doTestClick = nil
end
return true
end
-- put this button in some menu (i like RIGHT_SIDEBAR_BOTTOM)
button
{
area 0 55 73 55
-- the game constantly checks if the lua in enabled evaluates to true, so putting a function here will run it constantly
enabled "worldScreen == e:GetActiveEngine() and showJournal == 0 and testFuncRepeater()"
bam GUILS10
sequence 15
clickable lua "sidebarsGreyed ~= 1"
action "testFunc()"
}
Press that button, and it'll virtually click the nearest NEUTRAL! Combine it with Thievery, and you get a one-button-press of..
function testFunc()
for i=0,11 do if (actionBarTooltip[i]:match("Thieving")) then buttonArray:OnLButtonPressed(i) break end
if i == 11 then return end end -- abort if reached last actionbar w/out thieving. probably adjust to loop through special abilities too for cleric/thief?
....
..wow, it doesn't even automatically succeed? What a waste of time to code :P
And this works even in multiplayer, unlike the last bit I did with C:Eval().
EDIT: Although it doesn't play very nice with RMB-ing the action bar (even with an overridden function), some jank with certain order-of-operations involving right-clicking is preventing that from working, although it still works just fine even if I call "actionBarRMB(4)" from my test button. Weeeeeeird.
Seems like you are better at this than I am... and I was looking at the assembly code! Gah, that actually does make sense that these are “combo” functions. I noticed that the click functions were sending mouse down / mouse up events, and didn’t think the engine would care about / need mouse move events...
Good detective work; can’t imagine how long you must have been poking those functions. Together we will make the engine bow to our hackery,
Good detective work; can’t imagine how long you must have been poking those functions. Together we will make the engine bow to our hackery,
Took me about five minutes after I read your response -- there is zero documentation of most of these functions, so once you confirmed it did take x,y I set about tinkering around more, then I recalled that in the past, I had some other issues with positioning while hovering over other menus, sooo.. Slapped it on a hotkey and noticed it did in fact work so my next task was to figure out how to get the game to "think" that I'm in the world instead of over a menu. Luckily my first attempt, going straight to a 'hover' func, worked. >_>
Figuring out how to hack things to do what I want when it's clearly not meant to do that is one of my favorite hobbies! EEex really opens up a lot more hacky entry points, hehe.
I've finished implementing IWD2 style spontaneous casting system for clerics and noticed some oddities with hotkey listeners that I'm not sure how to deal with. Here is description how the system works:
Spontaneous Casting: Clerics can convert their spells to cure (if good or neutral) or inflict (if evil) spells of the nearest level for as long as a player presses and holds shift key. The selected spell is immediately removed from the character's memorized list and cast as the nearest cure or inflict spell of the same level. If no cure or inflict spell is available for that level, the highest spell under the converted spell level is used.
And here is a code that handles the auto spell appling based on Shift key button press and release (single spell is enough to activate this system since it uses opcode 191 to change how spells behave - based on @Pecca's idea)
K4_LastSpontaneousActorID = nil
function K4_SpontaneousKeyPressedListener(key)
if worldScreen == e:GetActiveEngine() and buttonArray:GetButtonEnabled(6) and key == 0x400000E1 then
local actorID = EEex_GetActorIDSelected()
-- existing actorID with CLERIC class (commented out for easier testing)
if actorID ~= 0x0 --[[and EEex_IsMaskSet(EEex_GetActorStat(actorID, %STAT_CLASSMASK%), K4_classMaskTable["CLERIC"][1])]] then
--Infinity_DisplayString("DEBUG: Spontaneous casting active for " .. EEex_GetActorName(actorID))
-- following line doesn't seem like a sane check, not really sure what GetButtonType number actually represents
if buttonArray:GetButtonType(6) > 11 then Infinity_PressKeyboardButton('escape') end
EEex_LuaObject = actorID
C:Eval('ActionOverride(EEex_LuaObject,ApplySpellRES("K#SPONTC",Myself))')
K4_LastSpontaneousActorID = actorID
end
end
end
EEex_AddKeyPressedListener(K4_SpontaneousKeyPressedListener)
function K4_SpontaneousKeyReleasedListener(key)
if worldScreen == e:GetActiveEngine() and key == 0x400000E1 and K4_LastSpontaneousActorID ~= nil then
--Infinity_DisplayString("DEBUG: Spontaneous casting disabled for " .. EEex_GetActorName(K4_LastSpontaneousActorID))
if buttonArray:GetButtonType(6) > 11 then Infinity_PressKeyboardButton('escape') end
EEex_LuaObject = K4_LastSpontaneousActorID
-- K#SPONT0 removes K#SPONTC effects
C:Eval('ActionOverride(EEex_LuaObject,ApplySpellRES("K#SPONT0",Myself))')
K4_LastSpontaneousActorID = nil
end
end
EEex_AddKeyReleasedListener(K4_SpontaneousKeyReleasedListener)
The first problem is that KeyReleasedListener returns true as soon as another button is pressed and released, so for example activating pause via space while still holding shift disables Spontaneous Casting. Any ideas how to solve it?
Now the weird part - for some reason C:Eval in K4_SpontaneousKeyReleasedListener doesn't work when the button is released while the cursor hovers over Watchers in candlekeep. Watchers seems to be only ones causing this issue - no problem when the mouse cursor is over Phyldi, Tutors etc.
edit: fixed the first part, reference code updated.
Sorry for breaking the chain of smart comments and alien coding but @Bubb can you:
Insert new classes to the game? Three new ones (Warlock, Favored Soul, Psionic), to be precise.
Create an Opcode that freezes time for everything but the spell that contains it (or is cast through it - this is to avoid a permanent freeze, of course)?
-- following line doesn't seem like a sane check, not really sure what GetButtonType number actually represents
buttonArray:GetButtonType(buttonIndex) is the same thing as EEex_GetActionbarButton(buttonIndex) - that is to say, each actionBar "choice" has a different type assigned to it. From M__EEex.lua:
But there is also 31-42 representing the twelve slots of "alternate menus" such as Special Abilities or Cast Spell/Use Item, 16-20 represents the various formation types, 67/69 represents the 'Jump to Mage/Cleric Spells' buttons.
@swit: Ahh, right... C:Eval executes the given action as if it was the creature under the cursor, if there is one. It doesn't work on watchers because their script is constantly random walking, not giving an opening to the eval.
That's a problem; running actions from Lua should not be dependent on the cursor. I'll either see to finally getting dedicated EEex action functions, or overriding Eval's behavior with a new arg.
@swit: Ahh, right... C:Eval executes the given action as if it was the creature under the cursor, if there is one. It doesn't work on watchers because their script is constantly random walking, not giving an opening to the eval.
That's a problem; running actions from Lua should not be dependent on the cursor. I'll either see to finally getting dedicated EEex action functions, or overriding Eval's behavior with a new arg.
I'm on mobile so I can't really get into it now but you can use action override script command to target specific peeps: C:Eval('ActionOverride([PC], "dowhatever()")'). Which is what swit is doing, but it still doesn't work. It SHOULD be forcing whichever and whatever object is being affected by eval to force selected actor to do the command, but..
There is still the separate issue of c:eval not working for clients in multiplayer though.
Level: 2
Sphere: Guardian, Creation
Range: Visual range of the caster
Duration: 1 turn
Casting Time: 2
Area of Effect: 1 creature
Saving Throw: None
Upon completion of this spell, a large hemispherical shell resembling that of a tortoise encases the target creature, shielding it from the outside world. The creature encased can take no action but in turn is impervious to all effects. The tortoise shell has 100 hit points and will remain on the target until <PRO_HESHE> is affected by a Dispel Magic, Tortoise Shell breaks, or the duration expires.
While working on the above spell I've noticed that there is currently no way to check actorID current HP from within Lua. Due to way this spell works (100 HP, none of the current HP should be healed when the shell breaks, no knocking unconscious like in the Rage ability that uses Damage opcode) I've decided to implement it with Invoke Lua opcode, triggered by the spell and on each hit the spell is active. I'm currently using this code to get the current HP:
characters[EEex_LuaObject].HP.current
but it's limited to just Player's party and requires opening record screen to update the data. Also tried to read it using stat 0 since splprot.2da works that way, but it doesn't work here. I'd like to request dedicated EEex function for current HP fetching, if it's not too hard to implement.
While working on the above spell I've noticed that there is currently no way to check actorID current HP from within Lua. Due to way this spell works (100 HP, none of the current HP should be healed when the shell breaks, no knocking unconscious like in the Rage ability that uses Damage opcode) I've decided to implement it with Invoke Lua opcode, triggered by the spell and on each hit the spell is active. I'm currently using this code to get the current HP:
characters[EEex_LuaObject].HP.current
but it's limited to just Player's party and requires opening record screen to update the data. Also tried to read it using stat 0 since splprot.2da works that way, but it doesn't work here. I'd like to request dedicated EEex function for current HP fetching, if it's not too hard to implement.
In my experiments, I found the offset that stores a creature's current HP.
For everyone's information, if you want to retrieve stats from the cre file of a particular actor, the cre file data is stored starting somewhere around EEex_GetActorShare(actorID) + 0x418. For example, the creature's flags (e.g. permanent corpse, nightmare mode enabled) are at offset 0x424. The creature's animation is at offset 0x43C. You can figure out other offsets by looking at the offsets in a cre file.
Technically should be using EEex_ReadSignedWord(), as the engine defines that it can be negative. And, of course I'll make it a dedicated function soon; don't want you guys to have to manage the memory offsets yourself
Actually, I seem to remember party member's HPs being able to go "negative", with the text on the portrait becoming red, but can't reproduce it. Anyone / (*cough* @kjeron *cough*) know if I'm remembering right?
Actually, I seem to remember party member's HPs being able to go "negative", with the text on the portrait becoming red, but can't reproduce it. Anyone / (*cough* @kjeron *cough*) know if I'm remembering right?
I know it can be done with op208, setting minimum HP > 32767 results in a red negative HP display, and a grey portrait. It also kills them, but whatever.
@Swit How complex is the Tortoise Shell implementation? Just curious to compare to what it would take without EEex. (roughly 3 files per current HP amount supported, so 1-255 HP takes 765 files, and that's with a static 100 HP absorbed)
@kjeron, 3 spells (1 with effects, 1 with remove effects by resource and invoke lua, additional spell with invoke lua applied on each hit) + this lua code (works ok, although I'm sure it could be still simplified)
--Tortoise Shell (IWPR222)
function EEex_GetActorCurrentHitPoints(actorID) --remove it once it's added to EEex library
return EEex_ReadWord(EEex_GetActorShare(actorID) + 0x438, 0x0)
end
function K4_TSSTR(effectData, creatureData)
EEex_LuaObject = EEex_ReadDword(creatureData + 0x34)
local hpcur = EEex_GetActorCurrentHitPoints(EEex_LuaObject)
C:Eval('ActionOverride(EEex_LuaObject,SetGlobal("K#TORTHP","LOCALS",' .. (hpcur - 100) .. '))')
C:Eval('ActionOverride(EEex_LuaObject,SetGlobal("K#TORTCU","LOCALS",' .. hpcur .. '))')
C:Eval('ActionOverride(EEex_LuaObject,SetGlobal("K#TORTSH","LOCALS",100))')
C:Eval('ActionOverride(EEex_LuaObject,ApplySpellRES("IWPR222A",Myself))')
end
function K4_TSHIT(effectData, creatureData)
EEex_LuaObject = EEex_ReadDword(creatureData + 0x34)
local hpcur = EEex_GetActorCurrentHitPoints(EEex_LuaObject)
local hpshell = EEex_GetActorLocal(EEex_LuaObject, "K#TORTSH")
local dmg = EEex_GetActorLocal(EEex_LuaObject, "K#TORTCU") - hpcur
if dmg >= hpshell then
C:Eval('ActionOverride(EEex_LuaObject,ApplySpellRES("IWPR222C",Myself))')
elseif dmg > 0 then
hpshell = hpshell - dmg
C:SetGlobal("TORTOISE_SHELL_HP","GLOBAL",hpshell)
C:Eval('SetTokenGlobal("TORTOISE_SHELL_HP","GLOBAL","TORTOISE_SHELL")')
C:Eval('DisplayString(EEex_LuaObject,172075)') --Tortoise Shell HP <TORTOISE_SHELL>/100
C:Eval('ActionOverride(EEex_LuaObject,SetGlobal("K#TORTCU","LOCALS",' .. hpcur .. '))')
C:Eval('ActionOverride(EEex_LuaObject,SetGlobal("K#TORTSH","LOCALS",' .. hpshell .. '))')
end
end
function K4_TSEND(effectData, creatureData)
EEex_LuaObject = EEex_ReadDword(creatureData + 0x34)
local hpcur = EEex_GetActorCurrentHitPoints(EEex_LuaObject)
local hpsav = EEex_GetActorLocal(EEex_LuaObject, "K#TORTHP")
if hpcur > hpsav then
C:Eval('ActionOverride(EEex_LuaObject,ApplyDamage(EEex_LuaObject,' .. (hpcur - hpsav) .. ',STUNNING))')
end
C:Eval('ActionOverride(EEex_LuaObject,SetGlobal("K#TORTCU","LOCALS",0))')
C:Eval('ActionOverride(EEex_LuaObject,SetGlobal("K#TORTHP","LOCALS",0))')
C:Eval('ActionOverride(EEex_LuaObject,SetGlobal("K#TORTSH","LOCALS",0))')
end
Any advice on detecting (a) target (either {x,y} or actorId depending on if ground or unit target) or (b) current combat counter (besides scripts' CombatCounter(x))? Can't save in combat, and loading wipes targets, so I don't think I can refer to the save file for these. >_>
EEex_IsActorInCombat(actorID) - Returns true if the engine is in the "combat" state. Basically whenever the engine says you can't save because you are in combat...
EEex_GetActorTargetID(actorID) - Returns the actorID of the given actor's target, or 0x0 if the actor isn't currently targeting another actor.
EEex_GetActorTargetPoint(actorID) - Returns the x, y point of the given actor's target ground-destination, or -1, -1 if the actor isn't current targeting the ground. Actually, erm, this only gives you the walk-destination of the actor, not the action's target point. I'll fix it tomorrow...
In addition to that, the memorized spell functions now fill the spell name and description in. Still working on getting the icon right
Comments
So, how do I get the ability icon instead of the spell icon? ... it's complicated, very complicated. And of course the engine doesn't have a function defined for the purpose; instead it inlined that functionality into CGameSprite::GetQuickButtons() -_-
I'll have the name / description stuff up once I figure out how to calculate the correct quick-spell icon...
Edit - even the game doesn't always generate the correct icon for spells in the actionbar, as it will default to the previous/next spell's icon if the spell doesn't have an ability less than or equal to the casters level.
I would love to know how, in terrible detail, you did this @Serity . I don't mind gross as long as the gross is behind the scenes (heh, you should see my code... ).
Edit: Also, I don't mind if you pretend I'm dumb. In fact, I prefer it, as I only understood some embarrassing fraction of what you just said :P
The spell-list is hard-coded with a priority order (if you don't have earlier spells, it checks later spells) because I couldn't think of a good organic method to set them in-game and because it's pretty much just for my (and friends') own personal use. Adjust that somehow if you want (there are a few ways you could do that but I'm not sure how to save it to your player data - you could save it to INI like 'Extra Slot Skill','Charactername','SPCL412', I guess).
Here's the code I used for that. I just wrote up the comments on the fly (because I'm a terrible no-commenter) so some things may be a little weird description-wise but it should be understandable.
https://pastebin.com/1tM08UHa
Notes: It uses Lefreut's "spell description" pane for the actionAlt (RMB) with a change: "Infinity_FetchString(currentSpell.name/.description)" labels (in the SPELL_DESCRIPTION menu) are switched out for getFetchOrDefault(currentSpell.name/.description), which is just this: .. so that it looks up a string resref, but if it finds no result (for example if you throw "Hello world" at the function instead of 28000), it will just return the text you sent it instead of nothing.
If you don't use Lefreut's thennnn.. uh. I dunno. Comment out the actionAlt I guess. Or figure out how to make it work with your UI :P
You could expand this if you want - for example, make the 'extra slot' actually pop out into several "favorited spells" which has the UI work the same way, just take the first listed spell found, second, third..
The hacky, (yet vanilla friendly!), way to close the submenu would be:
What I'm currently using in my local WIP EEex version is:
This reverts the actionbar to the previous state. (It'll become its own function eventually)
My own personal code also includes a cooldown meter for the extra slot (just CastTimer) based on your work but I didn't include that above since including that's stretching it a bit more out of scope and into its own thing (I just used Infinity_SetArea on fill elements as I mentioned before).
As a cute little addition to Thievery, I made it so if you RMB the Thieving button, it uses Pick Pockets on the nearest nearby neutral NPC (and it displays what you stole in the chatlog too, although that's extremely hacky as well and not included in the spoiler - scan inventory when toggling Thieving [sequence 26/28] or RMBing, then upon catching "Pick Pockets Succeeded" in chat log, scan inventory again and print the difference).
Sadly, it doesn't work unless you're in single-player or the host since C:Eval only works for the host =_= But this can be gotten around with some.. uh. Disgusting chat-box parsing/manipulation I guess? Have RMB send a chat message, have code watch the chatbox/combatlog for "!pickpockets actorId", remove the text, and if you're the host, execute the above code. Gross.
edit: How the heck do Infinity_[ClickObjectInWorld/ClickWorldAt/ClickScreen]() work? Nothing I'm putting in seems to work (actor IDs, x/y, etc) and just clicks at 0,0. If that worked I could maybe have it do [ThieveryLClick] -> Click nearby target..
Infinity_ClickObjectInWorld() takes a script name, (as in what is defined at offset 0x280 of the .CRE), as its only param. Contrary to the function name, it doesn't click the object, but the ground at its feet. Used like so to click the ground near Minsc:
Infinity_ClickWorldAt() takes x and y as parameters. Is supposed to click ground using the x, y as relative coordnates to the viewscreen, but appears entirely broken. It seems to always click the top-left of the current viewscreen.
Infinity_ClickScreen() takes no parameters and clicks the center of the viewscreen.
Infinity_ClickItem() takes a menuItem userdata type and clicks the center of its area. Used like this:
Note that this doesn't play button animations; it plays the click sound and does the action.
...not.
Infinity_HoverMouseOver(x,y) "moves" where your mouse cursor is in the game world. It doesn't ACTUALLY move it, but the game engine thinks it does. So if you combine them like so, you can force a click in the game world:
..so, you can fix that up to insta-click on an NPC with something like..
Press that button, and it'll virtually click the nearest NEUTRAL! Combine it with Thievery, and you get a one-button-press of..
..wow, it doesn't even automatically succeed? What a waste of time to code :P
And this works even in multiplayer, unlike the last bit I did with C:Eval().
EDIT: Although it doesn't play very nice with RMB-ing the action bar (even with an overridden function), some jank with certain order-of-operations involving right-clicking is preventing that from working, although it still works just fine even if I call "actionBarRMB(4)" from my test button. Weeeeeeird.
Good detective work; can’t imagine how long you must have been poking those functions. Together we will make the engine bow to our hackery,
Figuring out how to hack things to do what I want when it's clearly not meant to do that is one of my favorite hobbies! EEex really opens up a lot more hacky entry points, hehe.
The first problem is that KeyReleasedListener returns true as soon as another button is pressed and released, so for example activating pause via space while still holding shift disables Spontaneous Casting. Any ideas how to solve it?
Now the weird part - for some reason C:Eval in K4_SpontaneousKeyReleasedListener doesn't work when the button is released while the cursor hovers over Watchers in candlekeep. Watchers seems to be only ones causing this issue - no problem when the mouse cursor is over Phyldi, Tutors etc.
edit: fixed the first part, reference code updated.
Insert new classes to the game? Three new ones (Warlock, Favored Soul, Psionic), to be precise.
Create an Opcode that freezes time for everything but the spell that contains it (or is cast through it - this is to avoid a permanent freeze, of course)?
Tag me when you do the endless kit release?
Thanks in advance!
That's a problem; running actions from Lua should not be dependent on the cursor. I'll either see to finally getting dedicated EEex action functions, or overriding Eval's behavior with a new arg.
I'm on mobile so I can't really get into it now but you can use action override script command to target specific peeps: C:Eval('ActionOverride([PC], "dowhatever()")'). Which is what swit is doing, but it still doesn't work. It SHOULD be forcing whichever and whatever object is being affected by eval to force selected actor to do the command, but..
There is still the separate issue of c:eval not working for clients in multiplayer though.
While working on the above spell I've noticed that there is currently no way to check actorID current HP from within Lua. Due to way this spell works (100 HP, none of the current HP should be healed when the shell breaks, no knocking unconscious like in the Rage ability that uses Damage opcode) I've decided to implement it with Invoke Lua opcode, triggered by the spell and on each hit the spell is active. I'm currently using this code to get the current HP: but it's limited to just Player's party and requires opening record screen to update the data. Also tried to read it using stat 0 since splprot.2da works that way, but it doesn't work here. I'd like to request dedicated EEex function for current HP fetching, if it's not too hard to implement.
In my experiments, I found the offset that stores a creature's current HP.
For everyone's information, if you want to retrieve stats from the cre file of a particular actor, the cre file data is stored starting somewhere around EEex_GetActorShare(actorID) + 0x418. For example, the creature's flags (e.g. permanent corpse, nightmare mode enabled) are at offset 0x424. The creature's animation is at offset 0x43C. You can figure out other offsets by looking at the offsets in a cre file.
Actually, I seem to remember party member's HPs being able to go "negative", with the text on the portrait becoming red, but can't reproduce it. Anyone / (*cough* @kjeron *cough*) know if I'm remembering right?
@Swit How complex is the Tortoise Shell implementation? Just curious to compare to what it would take without EEex. (roughly 3 files per current HP amount supported, so 1-255 HP takes 765 files, and that's with a static 100 HP absorbed)
EEex_IsActorInCombat(actorID) - Returns true if the engine is in the "combat" state. Basically whenever the engine says you can't save because you are in combat...
EEex_GetActorTargetID(actorID) - Returns the actorID of the given actor's target, or 0x0 if the actor isn't currently targeting another actor.
EEex_GetActorTargetPoint(actorID) - Returns the x, y point of the given actor's target ground-destination, or -1, -1 if the actor isn't current targeting the ground. Actually, erm, this only gives you the walk-destination of the actor, not the action's target point. I'll fix it tomorrow...
In addition to that, the memorized spell functions now fill the spell name and description in. Still working on getting the icon right