|
addict
|
OP
addict
Joined: Sep 2015
|
A request on Ameranth's and Elric's Epic Encounters Nexus site reminded me of some old wounds I still suffer:
Several times I tried to teach NPCs in recognizing items blocking their path and in removing them, both in combat and, not least, during their peace routines. While it's mostly just 'fair' that the Evil gets trapped by the Good, I felt quite pity about those innocent NPCs that got locked in by dark-minded players, to be utterly dependent on their caprice. I tried to help them but, as I admit, I always failed.
This time, though, I seem to be on the right track and got a prototype working, both in combat and in peace, which made NPCs successfully free theirselves from their 'prisons' or remove items that stood between them and me. They aren't 100% accurate (and never will be) and sometimes there are some weird side-effects (like throwing items through walls or against another NPC's head), not exactly knowing which item is the one to move and where to throw it, but they managed to deliver quite well under different circumstances (blocking items close to themselves, close to the target or somewhere in between).
I had three test situations:
1) I placed items around Mayor Cecil's chair, where he uses to sit half the day, and watched if he would be able to reach his chair, starting from the cupboard near the stairs:
He walked to the barricades I had built, took the candle holder, throw it against his wife's head (no more than 1 damage, by the way) and walked close to his chair. And since I had placed two additional chairs to block the path twice, he took one chair, threw it away (well, once he threw his own chair right into the next room) and had a seat as though nothing had happened. I walled him in again, while he was relaxing, but he delivered as well.
2) There's a small hut in my testlevel, with two crates unfortunately blocking the entrance (the previous owner left it this way). I placed my player character inside the hut and then called several NPCs to come to me. They moved to the walls, walked around to the entrance, removed one crate, opened the door (thanks to the engine) and got in.
3) I started a fight in Morris's kitchen, some guards came in, I walked right out the door and placed two barrels at the door. Once a similar obstacle as a dragon, this time they just threw one barrel out of their way and attacked me.
I'm quite confident that there are still flaws or situations they can't handle or need some time for, doing stupid things. But I think I can already show it to you guys, so you can do some tests with it.
I've scripted two effects to see where the character wants to move to but fails in doing so (and to see which part of the script is being executed). The blue light shows the original target of the character when the movement fails. The red light shows an approximate position the character wants to move to when the movement fails. When you see the red light the character will try to move items close to his own position or close to the red light position.
Some comments on the script:
The main problems of doing this are:
1) We don't know the possible (but blocked) paths to the target. 2) We don't know where the blocking items are. 3) We don't know which items to regard (and can't or shouldn't check for every possible item template). 4) The scripting isn't really built for this specific problem. There is no OnMovementFailed EVENT, it's only a INTERRUPT event. But we necessarily need this. Recording every item the player moves or drops is not viable. Always checking for possible items that are in the way doesn't work either (we don't know the path and which items block it). Just checking for items the character is facing would be absolutely unreliable. So we have to add the OnMovementFailed lines to every reaction that contains movement (not much work for combat, but a lot of work for all NPCs of Main!).
So my idea was to move the character as close to the target as possible first. Since characters don't move at all when all paths are blocked, we don't know where we have to search for items. So I calculate a position half on the way to the target (slightly randomized, in case there's a wall or something) and let the character move there. If the movement fails the character will check for items he is facing (close to himself or close to the temporary target position) and move them, otherwise he will check for an even closer position and try to move there. He will ignore all items with weight less than 1000 and more than 40000 (I haven't checked all item weights yet, maybe there are a few that can block the path and are lighter than 1000/heavier than 40000). Maybe there will be conflicts with immobile items. In this case I will have to add more conditions. There could also be a problem with situations in which a character's movement fails for other reasons than items (don't know if this case exists). But all in all I'm quite happy about how it works already.
Now, I hope it works for you and would be glad about your feedback!
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
Here's the script for Cecil:
#INCLUDE State_Manager
INIT
USING State_Manager
CHARACTER:__Me
CHARACTER:%Victoria = CYS_OrcLibrarian_6a429053-8eb7-48d1-94cc-7292449487d7
TRIGGER:%pointBook = CYS_PointCecil1_faad94a4-5ae5-45bc-9b71-31510bc6cef5
TRIGGER:%pointCloset = CYS_Point_Cecil_Closet_37dac4f4-48e2-4ae7-b34d-349bf5273287
TRIGGER:%pointOutside = CYS_PointOrcLibOutside_e6945537-cff1-4699-b3f9-5d2a47ceed9d
ITEM:%cupboard = FUR_Rich_Cupboard_A_Door_A_002_ee9c92f9-ff31-41e9-965e-0128bf72d6b2
FLOAT3:%TargetPos
FLOAT3:%TempTarget
BEHAVIOUR
REACTION State_ActionManager, 0
USAGE PEACE
ACTIONS
GetWeightedRandom(%currentAction, "Action_ReadBook", INT:10, "Action_TalkToGuardsOutside", INT:4, "Action_LookIntoCloset", INT:4)
SetPriority(%currentAction, 1100)
REACTION State_VictoriaDead, 0
USAGE PEACE
VARS
FIXEDSTRING:_Animation
ACTIONS
CharacterMoveTo(%Victoria,1)
CharacterLookAt(%Victoria)
GetWeightedRandom(_Animation,"Crying_01",FLOAT:15,"Kneel_01",FLOAT:10,"Kneel_02",FLOAT:10,"Depressed_01",FLOAT:5)
CharacterPlayAnimation(_Animation)
Sleep(4)
DialogStart("CYS_AD_CecilMourning",__Me)
REACTION Action_ReadBook, 0
USAGE PEACE
VARS
FLOAT3:_Pos
ACTIONS
IF "!c1"
CharacterHasStatus(__Me,SITTING)
THEN
CharacterMoveTo(%pointBook, 0)
CharacterUseItem(CYS_CecilChair_acb98a79-1842-48ab-a86d-0fd1e177abaf)
ENDIF
CharacterPlayAnimation("Idle1")
CharacterPlayAnimation("Idle3")
Sleep(3)
CharacterPlayAnimation("Idle2")
SetPriority(%currentAction, 0)
INTERRUPT
Reset()
ON
OnMovementFailed(_Pos)
ACTIONS
Set(%TargetPos,_Pos)
PlayEffectAt(%TargetPos,"FX_Env_Fire_Blue_B")
SetPriority("GetClose",2000)
REACTION Action_TalkToGuardsOutside, 0
USAGE PEACE
ACTIONS
CharacterMoveTo(%pointOutside, 0)
CharacterLookFrom(%pointOutside)
Sleep(4)
SetPriority(%currentAction, 0)
REACTION Action_LookIntoCloset, 0
USAGE PEACE
VARS
FLOAT3:_Pos
ACTIONS
CharacterMoveTo(%pointCloset, 0)
CharacterLookFrom(%pointCloset)
CharacterPlayAnimation("Fidget_Forward_01")
ItemOpen(%cupboard)
CharacterPlayAnimation("Look_Left_Short_01")
CharacterPlayAnimation("Look_Right_Short_01")
ItemClose(%cupboard)
SetPriority(%currentAction, 0)
INTERRUPT
Reset()
ON
OnMovementFailed(_Pos)
ACTIONS
Set(%TargetPos,_Pos)
PlayEffectAt(%TargetPos,"FX_Env_Fire_Blue_B")
SetPriority("GetClose",2000)
REACTION GetClose, 0
USAGE PEACE
VARS
FLOAT:_distance
FLOAT:_MyX
FLOAT:_MyZ
FLOAT:_TargetX
FLOAT:_TargetZ
FLOAT:_AttemptCounter = 0
FLOAT:_AddX
FLOAT:_AddZ
FLOAT:_Divisor
FLOAT3:_Target
FLOAT3:_MyPos
ACTIONS
Sleep(1)
GetPosition(__Me,_MyPos)
Set(%TempTarget,%TargetPos)
IF "c1&c2&c3&c4&c5"
GetX(_MyPos,_MyX)
GetZ(_MyPos,_MyZ)
GetX(%TargetPos,_TargetX)
GetZ(%TargetPos,_TargetZ)
GetDistance(_distance,__Me,%TargetPos)
THEN
Subtract(_TargetX,_MyX)
Subtract(_TargetZ,_MyZ)
GetRandom(_Divisor,1.8,1.9,2,2.1,2.2)
Divide(_TargetX,_Divisor)
Divide(_TargetZ,_Divisor)
Add(_MyX,_TargetX)
Add(_MyZ,_TargetZ)
SetX(%TempTarget,_MyX)
SetZ(%TempTarget,_MyZ)
CharacterLookAt(%TempTarget)
IF "c1"
IsGreaterThen(_AttemptCounter,4)
THEN
Set(_AttemptCounter,0)
SetPriority("MoveItems",0)
CharacterEndTurn()
SetPriority("GetClose",0)
ELIF "c1"
IsLessThen(_distance,4)
THEN
CharacterMoveTo(%TargetPos,0,0,0,0.2,1)
SetPriority("GetClose",0)
ELIF "!c1"
IsLessThen(_distance,4)
THEN
CharacterMoveTo(%TempTarget,0,0,0,0.2,1)
SetPriority("MoveItems",0)
ELSE
Set(_AttemptCounter,0)
SetPriority("MoveItems",0)
SetPriority("GetClose",0)
ENDIF
ENDIF
INTERRUPT
Reset()
ON
OnMovementFailed(_)
ACTIONS
PlayEffectAt(%TempTarget,"FX_Env_Fire_Red_A")
Add(_AttemptCounter,1)
SetPriority("MoveItems",3000)
SetPriority("GetClose",0)
REACTION MoveItems, 0
USAGE PEACE
VARS
ITEM:_Item
ITEM:_IgnoreItem1
ITEM:_IgnoreItem2
ITEM:_IgnoreItem3
FLOAT:_Weight
FLOAT:_X
FLOAT:_Z
FLOAT:_MyPosX
FLOAT:_MyPosZ
FLOAT3:_MyPos
CHECK "(c1&c2&c3&c4&!c5&c6)|(c7&c8&c9&!c10&c11)"
ItemGet(_Item,__Me,6,Random,Distance,"")
IsFacing(__Me,_Item)
CanSee(__Me,_Item)
ItemGetStat(_Weight,_Item,Weight)
IsLessThen(_Weight,1000)
IsLessThen(_Weight,40001)
ItemGet(_Item,%TempTarget,6,Random,Distance,"")
IsFacing(__Me,_Item)
ItemGetStat(_Weight,_Item,Weight)
IsLessThen(_Weight,1000)
IsLessThen(_Weight,40001)
ACTIONS
IF "!c1"
ItemIsDestroyed(_Item)
THEN
CharacterMoveTo(_Item)
ENDIF
GetPosition(__Me,_MyPos)
GetRandom(_X,-3,-2,2,3)
GetRandom(_Z,-3,-2,2,3)
IF "c1&c2"
GetX(_MyPos,_MyPosX)
GetZ(_MyPos,_MyPosZ)
THEN
Subtract(_MyPosX,_X)
Subtract(_MyPosZ,_Z)
SetX(_MyPos,_MyPosX)
SetZ(_MyPos,_MyPosZ)
ENDIF
IF "!c1"
ItemIsDestroyed(_Item)
THEN
CharacterMoveItem(_Item,_MyPos)
ENDIF
SetPriority("MoveItems",0)
INTERRUPT
Reset()
SetPriority("MoveItems",0)
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
And the DefaultCharacter script, for combat tests:
#INCLUDE Base
INIT
USING Base
CHARACTER:__Me
CHARACTER:%DialogLookAtChar=null
FLOAT3:%PeaceReturnPosition=null
FLOAT:%setTargetDefaultBestScore=10000
INT:%defaultEvaluateTarget=1
INT:%EvaluateScores=0
FLOAT3:%TargetPos
FLOAT3:%TempTarget
EVENTS
EVENT TauntedByChar
VARS
CHARACTER:_Char
ON
OnCharacterStatus(__Me,TAUNTED)
ACTIONS
IF "c1"
CharacterGetStatusSourceCharacter(__Me,TAUNTED,_Char)
THEN
Set(%defaultEvaluateTarget,0)
CharacterSetEnemy(__Me,_Char)
ELSE
CharacterRemoveStatus(__Me,TAUNTED)
ENDIF
EVENT TauntedFinished
ON
OnCharacterStatusRemoved(__Me,TAUNTED)
ACTIONS
CharacterSetEnemy(__Me,null)
Set(%defaultEvaluateTarget,1)
EVENT FallbackIterateTarget
VARS
CHARACTER:_Char
FLOAT:_targetDistance
FLOAT:_targetClosestThisScore=0
ON
OnIterateCharacter(_Char,"FallbackTargetClosestIterator")
ACTIONS
IF "!c1&!c2&c3&!c4&!c5&c6"
CharacterIsDead(_Char)
IsEqual(__Me,_Char)
CharacterIsEnemy(__Me,_Char)
CharacterHasStatus(_Char,INVISIBLE)
CharacterHasStatus(_Char,SNEAKING)
GetDistance(_targetDistance,__Me,_Char)
THEN
IF "c1&!c2"
CharacterHasTalent(_Char,Stench)
CharacterHasStatus(_Char,AGGRO_MARKED)
THEN
Multiply(_targetDistance,4)
ENDIF
IF "c1&c2"
CharacterHasTalent(_Char,Stench)
CharacterHasStatus(_Char,AGGRO_MARKED)
THEN
Multiply(_targetDistance,2)
ENDIF
Set(_targetClosestThisScore,_targetDistance)
IF "!c1"
IsGreaterThen(_targetClosestThisScore,%setTargetDefaultBestScore)
THEN
Set(%setTargetDefaultBestScore,_targetClosestThisScore)
CharacterSetEnemy(__Me,_Char)
ENDIF
ENDIF
EVENT DefaultIterateSetTarget
VARS
CHARACTER:_Char
CHARACTER:_previousSetTarget
FLOAT:_targetDistance
INT:_alreadyTargetedBy
FLOAT:_targetClosestThisScore=0
FLOAT:_movement
ON
OnIterateCharacter(_Char,"DefaultSetTargetClosestIterator")
ACTIONS
IF "!c1&!c2&c3&c4&c5&!c6&!c7"
CharacterIsDead(_Char)
IsEqual(__Me,_Char)
CharacterIsEnemy(__Me,_Char)
GetDistance(_targetDistance,__Me,_Char)
CharacterGetHostileCount(_alreadyTargetedBy,_Char)
CharacterHasStatus(_Char,INVISIBLE)
CharacterHasStatus(_Char,SNEAKING)
THEN
IF "c1&c2"
CharacterGetEnemy(_previousSetTarget,__Me)
IsEqual(_previousSetTarget,_Char)
THEN
Multiply(_targetDistance,0.5)
ENDIF
IF "c1&!c2&!c3"
IsInDangerousSurface(_Char)
CharacterIsFloating(__Me)
CharacterHasStatus(__Me,ETHEREAL_SOLES)
THEN
Multiply(_targetDistance,2)
ENDIF
IF "c1&(c2|c3)"
IsInDangerousSurface(_Char)
CharacterIsFloating(__Me)
CharacterHasStatus(__Me,ETHEREAL_SOLES)
THEN
Multiply(_targetDistance,0.8)
ENDIF
IF "c1"
CharacterIsSummon(_Char)
THEN
Multiply(_targetDistance,1.5)
ENDIF
IF "c1"
CharacterHasStatus(_Char,AGGRO_MARKED)
THEN
Multiply(_targetDistance,0.2)
ENDIF
IF "c1"
IsEqual(_alreadyTargetedBy,0)
THEN
Multiply(_targetDistance,0.5)
ENDIF
IF "c1"
IsEqual(_alreadyTargetedBy,1)
THEN
Multiply(_targetDistance,0.6)
ENDIF
IF "c1&!c2"
IsGreaterThen(_alreadyTargetedBy,1)
CharacterHasStatus(_Char,AGGRO_MARKED)
THEN
Add(_alreadyTargetedBy,0.6)
Multiply(_alreadyTargetedBy,_alreadyTargetedBy)
Multiply(_targetDistance,_alreadyTargetedBy)
ENDIF
IF "c1&!c2"
IsGreaterThen(_alreadyTargetedBy,3)
CharacterHasStatus(_Char,AGGRO_MARKED)
THEN
Multiply(_targetDistance,2)
ENDIF
IF "!c1"
CharacterCanSee(__Me,_Char)
THEN
Multiply(_targetDistance,3)
ENDIF
IF "c1&!c2"
CharacterHasTalent(_Char,Stench)
CharacterHasStatus(_Char,AGGRO_MARKED)
THEN
Multiply(_targetDistance,4)
ENDIF
IF "c1&c2"
CharacterHasTalent(_Char,Stench)
CharacterHasStatus(_Char,AGGRO_MARKED)
THEN
Multiply(_targetDistance,2)
ENDIF
Set(_targetClosestThisScore,_targetDistance)
IF "!c1"
IsGreaterThen(_targetClosestThisScore,%setTargetDefaultBestScore)
THEN
Set(%setTargetDefaultBestScore,_targetClosestThisScore)
CharacterSetEnemy(__Me,_Char)
ENDIF
ENDIF
EVENT StartCombatFetchPos
ON
OnCombatStarted()
ACTIONS
CharacterSetEnemy(__Me,null)
Set(%defaultEvaluateTarget,1)
IF "c1"
IsEqual(%PeaceReturnPosition,null)
THEN
GetPosition(__Me,%PeaceReturnPosition)
CharacterForceUpdate(1)
ENDIF
EVENT ReturnToPosOnCombatEnded
ON
OnCombatEnded()
ACTIONS
IF "c1|c2"
CharacterIsInParty(__Me)
CharacterIsDead(__Me)
THEN
CharacterEvent(__Me,"ClearPeaceReturn")
ELSE
SetHealth(__Me,1)
ENDIF
EVENT StoryOverridePeaceReturn
ON
OnStoryOverride()
OnCharacterEvent(__Me,"ClearPeaceReturn")
ACTIONS
IF "!c1"
IsEqual(%PeaceReturnPosition,null)
THEN
Set(%PeaceReturnPosition,null)
Interrupt("ReturnToPeacePosition")
CharacterForceUpdate(0)
ENDIF
EVENT StartEvaluateScore
ON
OnFunction("StartEvaluateScore")
ACTIONS
IF "c1&c2"
IsEqual(%EvaluateScores,0)
CharacterIsInActiveTurn(__Me)
THEN
CallFunction("CalculateScore")
StartTimer("CalcScoreTimer",0.2,-1)
Set(%EvaluateScores,1)
ENDIF
EVENT StopEvaluateScore
ON
OnFunction("StopEvaluateScore")
ACTIONS
IF "c1"
IsEqual(%EvaluateScores,1)
THEN
StopTimer("CalcScoreTimer")
Set(%EvaluateScores,0)
ENDIF
EVENT DontAttackAlliesOrInvisibles // to solve charmed chars getting back to normal but still targeting as if they were charmed
VARS
CHARACTER:_Target
ON
OnTurn()
ACTIONS
IF "c1&(c2|c3)"
CharacterGetEnemy(_Target,__Me)
CharacterIsAlly(__Me,_Target)
CharacterHasStatus(_Target,INVISIBLE)
THEN
CharacterSetEnemy(__Me,null)
Set(%defaultEvaluateTarget,1)
ENDIF
EVENT ClearTauntedWhenTaunterDies
VARS
CHARACTER:_EventChar=null
CHARACTER:_Taunter=null
ON
OnDie(_EventChar,_,_,_)
ACTIONS
IF "c1&c2&c3"
CharacterHasStatus(__Me,TAUNTED)
CharacterGetStatusSourceCharacter(__Me,TAUNTED,_Taunter)
IsEqual(_Taunter,_EventChar)
THEN
CharacterRemoveStatus(__Me,TAUNTED)
ENDIF
EVENT ClearSetEnemy
VARS
CHARACTER:_Char
CHARACTER:_EventChar
ON
OnDie(_EventChar,_,_,_)
OnCharacterStatus(_EventChar,INVISIBLE)
OnCharacterStatus(_EventChar,CHARMED)
OnCharacterStatus(_EventChar,SNEAKING)
ACTIONS
IF "c1&c2"
CharacterGetEnemy(_Char,__Me)
IsEqual(_Char,_EventChar)
THEN
CharacterSetEnemy(__Me,null)
Set(%defaultEvaluateTarget,1)
ENDIF
EVENT ForceNewEnemyWhenCharmed
ON
OnCharacterStatus(__Me,CHARMED)
ACTIONS
CharacterSetEnemy(__Me,null)
Set(%defaultEvaluateTarget,1)
EVENT EvaluateTargets
ON
OnTurn()
ACTIONS
IF "!c1"
CharacterHasStatus(__Me,TAUNTED)
THEN
SetPriority("Combat_DefaultChooseEnemy",9)
SetPriority("Combat_NoSetEnemy",6)
Set(%setTargetDefaultBestScore,10000)
Set(%defaultEvaluateTarget,1)
CallFunction("StartEvaluateScore")
SetPriority("Combat_WhenInvisible_RunAway",10)
ENDIF
EVENT KillScoreTimer
ON
OnTurnEnded()
OnCombatEnded()
ACTIONS
CallFunction("StopEvaluateScore")
EVENT CalcScoreTimer
ON
OnTimer("CalcScoreTimer")
ACTIONS
CallFunction("CalculateScore")
//----
BEHAVIOUR
REACTION ReturnToPeacePosition,15000
USAGE PEACE
CHECK "!c1"
IsEqual(%PeaceReturnPosition,null)
ACTIONS
CharacterMoveTo(%PeaceReturnPosition,1,1,1,0)
CharacterEvent(__Me,"ClearPeaceReturn")
REACTION Combat_WhenInvisible_RunAway, 10
USAGE COMBAT
VARS
FLOAT:_MyVitality
CHECK "c1"
CharacterHasStatus(__Me,"INVISIBLE")
ACTIONS
IF "(c1&c2)|c3"
CharacterGetStat(_MyVitality,__Me,Vitality)
IsLessThen(_MyVitality,0.5)
IsRandom(0.5)
THEN
CharacterFleeFrom(Enemy,10)
ELSE
SetPriority("Combat_WhenInvisible_RunAway",0)
ENDIF
INTERRUPT
ON
OnMovementFailed(_)
ACTIONS
CharacterEndTurn()
REACTION Combat_DefaultChooseEnemy, 9
USAGE COMBAT
VARS
CHARACTER:_Enemy
CHECK "c1|!c2"
IsEqual(%defaultEvaluateTarget,1)
CharacterGetEnemy(_Enemy,__Me)
ACTIONS
Set(%setTargetDefaultBestScore,10000)
IterateCharactersInCombat(__Me,"DefaultSetTargetClosestIterator")
IF "c1"
CharacterGetEnemy(_Enemy,__Me)
THEN
Set(%defaultEvaluateTarget,0)
ELSE
SetPriority("Combat_DefaultChooseEnemy",0)
ENDIF
REACTION Combat_AttackSetEnemy, 8
USAGE COMBAT
VARS
CHARACTER:_Enemy
FLOAT:_dist
FLOAT:_minRange
FLOAT:_maxRange
FLOAT3:_Pos
CHECK "c1&c2&c3&(c4|(c5&!c6&!c7))"
CharacterGetEnemy(_Enemy,__Me) // returns false if null
CharacterGetWeaponRange(_minRange,_maxRange,__Me)
GetInnerDistance(_dist,__Me,_Enemy)
CharacterCanSee(__Me,_Enemy)
IsLessThen(_dist,_maxRange)
CharacterHasWeaponType(__Me,Bow,1)
CharacterHasWeaponType(__Me,CrossBow,1)
ACTIONS
CharacterAttack(_Enemy)
INTERRUPT
ON
OnMovementFailed(_Pos)
ACTIONS
Set(%TargetPos,_Pos)
PlayEffectAt(%TargetPos,"FX_Env_Fire_Blue_B")
SetPriority("GetClose",2000)
REACTION Combat_AttackSetEnemyWithBowHack, 8
USAGE COMBAT
VARS
CHARACTER:_Enemy
FLOAT:_dist
FLOAT:_minRange
FLOAT:_maxRange
FLOAT3:_Pos
CHECK "c1&c2&c3&!c4&c5&(c6|c7)"
CharacterGetEnemy(_Enemy,__Me) // returns false if null
CharacterGetWeaponRange(_minRange,_maxRange,__Me)
GetInnerDistance(_dist,__Me,_Enemy)
CharacterCanSee(__Me,_Enemy)
IsLessThen(_dist,2)
CharacterHasWeaponType(__Me,Bow,1)
CharacterHasWeaponType(__Me,CrossBow,1)
ACTIONS
CharacterAttack(_Enemy)
INTERRUPT
ON
OnMovementFailed(_Pos)
ACTIONS
Set(%TargetPos,_Pos)
PlayEffectAt(%TargetPos,"FX_Env_Fire_Blue_B")
SetPriority("GetClose",2000)
REACTION Combat_MoveToSetEnemy, 7
USAGE COMBAT
VARS
CHARACTER:_Enemy
FLOAT:_minRange
FLOAT:_maxRange
FLOAT3:_Pos
CHECK "c1&!c2&c3"
CharacterGetEnemy(_Enemy,__Me)
CharacterCanSee(__Me,_Enemy)
CharacterGetWeaponRange(_minRange,_maxRange,__Me)
ACTIONS
CharacterMoveTo(_Enemy,1,0,0,_minRange,_maxRange)
INTERRUPT
ON
OnMovementFailed(_Pos)
ACTIONS
Set(%TargetPos,_Pos)
PlayEffectAt(%TargetPos,"FX_Env_Fire_Blue_B")
SetPriority("GetClose",2000)
REACTION Combat_NoSetEnemy, 6
USAGE COMBAT
VARS
CHARACTER:_Char
CHECK "!c1"
CharacterGetEnemy(_Char,__Me)
ACTIONS
IterateCharactersInCombat(__Me,"FallbackTargetClosestIterator")
IF "!c1"
CharacterGetEnemy(_Char,__Me)
THEN
SetPriority("Combat_NoSetEnemy",0)
ENDIF
REACTION Combat_MoveOutOfSurfaces, 5
USAGE COMBAT
CHECK "(c1|c2)&!c3&!c4"
IsInSurface(__Me,Oil)
IsInDangerousSurface(__Me)
CharacterIsFloating(__Me)
CharacterHasStatus(__Me,ETHEREAL_SOLES)
ACTIONS
IF "c1"
IsInSurface(__Me,Oil)
THEN
CharacterFleeFromSurface(Oil)
ELIF "c1"
IsInDangerousSurface(__Me)
THEN
CharacterFleeFromDangerousSurface()
ENDIF
INTERRUPT
ON
OnException()
ACTIONS
DelayReaction("Combat_MoveOutOfSurfaces",6)
REACTION Combat_TakeCover, 2
USAGE COMBAT
VARS
FLOAT3:_Pos
FLOAT:_Value
CHECK "c1&c2&c3&c4&c5"
CharacterGetStat(_Value,__Me,Vitality)
IsLessThen(_Value,1)
FindCover(_Pos,__Me,__Me,20)
GetDistance(_Value,_Pos,__Me)
IsGreaterThen(_Value,1)
ACTIONS
CharacterPlayEffect(__Me,"FX_GP_QuestionMark_A")
CharacterEndTurn()
INTERRUPT
ON
OnMovementFailed(_)
ACTIONS
DelayReaction("Combat_TakeCover",3)
REACTION Combat_NoReactionFound, 1
USAGE COMBAT
ACTIONS
//CharacterPlayEffect(__Me,"FX_GP_QuestionMark_A")
CharacterEndTurn()
REACTION AutomatedDialog_Interrupt, 1700
USAGE PEACE
CHECK "c1&!c2"
IsInDialog(__Me)
CharacterIsPlayer(__Me)
ACTIONS
IF "!c1"
IsEqual(%DialogLookAtChar,null)
THEN
CharacterLookAt(%DialogLookAtChar,0)
ENDIF
Sleep(2.0)
INTERRUPT
Set(%DialogLookAtChar,null)
REACTION GetClose, 0
USAGE PEACE
USAGE COMBAT
VARS
FLOAT:_distance
FLOAT:_MyX
FLOAT:_MyZ
FLOAT:_TargetX
FLOAT:_TargetZ
FLOAT:_AttemptCounter = 0
FLOAT:_AddX
FLOAT:_AddZ
FLOAT:_Divisor
FLOAT3:_Target
FLOAT3:_MyPos
ACTIONS
Sleep(1)
GetPosition(__Me,_MyPos)
Set(%TempTarget,%TargetPos)
IF "c1&c2&c3&c4&c5"
GetX(_MyPos,_MyX)
GetZ(_MyPos,_MyZ)
GetX(%TargetPos,_TargetX)
GetZ(%TargetPos,_TargetZ)
GetDistance(_distance,__Me,%TargetPos)
THEN
Subtract(_TargetX,_MyX)
Subtract(_TargetZ,_MyZ)
GetRandom(_Divisor,1.8,1.9,2,2.1,2.2)
Divide(_TargetX,_Divisor)
Divide(_TargetZ,_Divisor)
Add(_MyX,_TargetX)
Add(_MyZ,_TargetZ)
SetX(%TempTarget,_MyX)
SetZ(%TempTarget,_MyZ)
CharacterLookAt(%TempTarget)
IF "c1"
IsGreaterThen(_AttemptCounter,4)
THEN
Set(_AttemptCounter,0)
SetPriority("MoveItems",0)
CharacterEndTurn()
SetPriority("GetClose",0)
ELIF "c1" //elif block added
IsLessThen(_distance,4)
THEN
CharacterMoveTo(%TargetPos,0,0,0,0.2,1)
SetPriority("GetClose",0)
ELIF "!c1"
IsLessThen(_distance,4)
THEN
CharacterMoveTo(%TempTarget,0,0,0,0.2,1)
SetPriority("MoveItems",0)
ELSE
Set(_AttemptCounter,0)
SetPriority("MoveItems",0)
SetPriority("GetClose",0)
ENDIF
ENDIF
INTERRUPT
Reset()
ON
OnMovementFailed(_)
ACTIONS
PlayEffectAt(%TempTarget,"FX_Env_Fire_Red_A")
Add(_AttemptCounter,1)
SetPriority("MoveItems",3000)
SetPriority("GetClose",0)
REACTION MoveItems, 0
USAGE PEACE
USAGE COMBAT
VARS
ITEM:_Item
ITEM:_IgnoreItem1
ITEM:_IgnoreItem2
ITEM:_IgnoreItem3
FLOAT:_Weight
FLOAT:_X
FLOAT:_Z
FLOAT:_MyPosX
FLOAT:_MyPosZ
FLOAT3:_MyPos
CHECK "(c1&c2&c3&c4&!c5&c6)|(c7&c8&c9&!c10&c11)"
ItemGet(_Item,__Me,6,Random,Distance,"")
IsFacing(__Me,_Item)
CanSee(__Me,_Item)
ItemGetStat(_Weight,_Item,Weight)
IsLessThen(_Weight,1000)
IsLessThen(_Weight,40001)
ItemGet(_Item,%TempTarget,6,Random,Distance,"")
IsFacing(__Me,_Item)
ItemGetStat(_Weight,_Item,Weight)
IsLessThen(_Weight,1000)
IsLessThen(_Weight,40001)
ACTIONS
IF "!c1"
ItemIsDestroyed(_Item)
THEN
CharacterMoveTo(_Item)
ENDIF
GetPosition(__Me,_MyPos)
GetRandom(_X,-3,-2,2,3)
GetRandom(_Z,-3,-2,2,3)
IF "c1&c2"
GetX(_MyPos,_MyPosX)
GetZ(_MyPos,_MyPosZ)
THEN
Subtract(_MyPosX,_X)
Subtract(_MyPosZ,_Z)
SetX(_MyPos,_MyPosX)
SetZ(_MyPos,_MyPosZ)
ENDIF
IF "!c1"
ItemIsDestroyed(_Item)
THEN
CharacterMoveItem(_Item,_MyPos)
ENDIF
SetPriority("MoveItems",0)
INTERRUPT
Reset()
SetPriority("MoveItems",0)
And here the whole code I added to both scripts:
//for the INIT section:
FLOAT3:%TargetPos
FLOAT3:%TempTarget
//To be added to every reaction that contains movement (very annoying, but there is no OnMovementFailed EVENT):
VARS
FLOAT3:_Pos
INTERRUPT
Reset()
ON
OnMovementFailed(_Pos)
ACTIONS
Set(%TargetPos,_Pos)
PlayEffectAt(%TargetPos,"FX_Env_Fire_Blue_B")
SetPriority("GetClose",2000)
//Reaction to get closer to the original target:
REACTION GetClose, 0
USAGE PEACE
VARS
FLOAT:_distance
FLOAT:_MyX
FLOAT:_MyZ
FLOAT:_TargetX
FLOAT:_TargetZ
FLOAT:_AttemptCounter = 0
FLOAT:_AddX
FLOAT:_AddZ
FLOAT:_Divisor
FLOAT3:_Target
FLOAT3:_MyPos
ACTIONS
Sleep(1)
GetPosition(__Me,_MyPos)
Set(%TempTarget,%TargetPos)
IF "c1&c2&c3&c4&c5"
GetX(_MyPos,_MyX)
GetZ(_MyPos,_MyZ)
GetX(%TargetPos,_TargetX)
GetZ(%TargetPos,_TargetZ)
GetDistance(_distance,__Me,%TargetPos)
THEN
Subtract(_TargetX,_MyX)
Subtract(_TargetZ,_MyZ)
GetRandom(_Divisor,1.8,1.9,2,2.1,2.2)
Divide(_TargetX,_Divisor)
Divide(_TargetZ,_Divisor)
Add(_MyX,_TargetX)
Add(_MyZ,_TargetZ)
SetX(%TempTarget,_MyX)
SetZ(%TempTarget,_MyZ)
CharacterLookAt(%TempTarget)
IF "c1"
IsGreaterThen(_AttemptCounter,4)
THEN
Set(_AttemptCounter,0)
SetPriority("MoveItems",0)
CharacterEndTurn()
SetPriority("GetClose",0)
ELIF "c1" //elif block added
IsLessThen(_distance,4)
THEN
CharacterMoveTo(%TargetPos,0,0,0,0.2,1)
SetPriority("GetClose",0)
ELIF "!c1"
IsLessThen(_distance,4)
THEN
CharacterMoveTo(%TempTarget,0,0,0,0.2,1)
SetPriority("MoveItems",0)
ELSE
Set(_AttemptCounter,0)
SetPriority("MoveItems",0)
SetPriority("GetClose",0)
ENDIF
ENDIF
INTERRUPT
Reset()
ON
OnMovementFailed(_)
ACTIONS
PlayEffectAt(%TempTarget,"FX_Env_Fire_Red_A")
Add(_AttemptCounter,1)
SetPriority("MoveItems",3000)
SetPriority("GetClose",0)
//Reaction to detect and move items:
REACTION MoveItems, 0
USAGE PEACE
VARS
ITEM:_Item
FLOAT:_Weight
FLOAT:_X
FLOAT:_Z
FLOAT:_MyPosX
FLOAT:_MyPosZ
FLOAT3:_MyPos
CHECK "(c1&c2&c3&c4&!c5&c6)|(c7&c8&c9&!c10&c11)"
ItemGet(_Item,__Me,6,Random,Distance,"")
IsFacing(__Me,_Item)
CanSee(__Me,_Item)
ItemGetStat(_Weight,_Item,Weight)
IsLessThen(_Weight,1000)
IsLessThen(_Weight,40001)
ItemGet(_Item,%TempTarget,6,Random,Distance,"")
IsFacing(__Me,_Item)
ItemGetStat(_Weight,_Item,Weight)
IsLessThen(_Weight,1000)
IsLessThen(_Weight,40001)
ACTIONS
IF "!c1"
ItemIsDestroyed(_Item)
THEN
CharacterMoveTo(_Item)
ENDIF
GetPosition(__Me,_MyPos)
GetRandom(_X,-3,-2,2,3)
GetRandom(_Z,-3,-2,2,3)
IF "c1&c2"
GetX(_MyPos,_MyPosX)
GetZ(_MyPos,_MyPosZ)
THEN
Subtract(_MyPosX,_X)
Subtract(_MyPosZ,_Z)
SetX(_MyPos,_MyPosX)
SetZ(_MyPos,_MyPosZ)
ENDIF
IF "!c1"
ItemIsDestroyed(_Item)
THEN
CharacterMoveItem(_Item,_MyPos)
ENDIF
SetPriority("MoveItems",0)
INTERRUPT
Reset()
SetPriority("MoveItems",0)
|
|
|
|
member
|
member
Joined: Dec 2016
|
Very nice work! I did something similar (and much less complex) with Epic Encounters' last patch, allowing enemies to identify and destroy Ice Walls when they are blocked by them. I wish I still had the time to dive into this topic, because you've given us a solid starting point with this.
Some thoughts:
- One could use FindValidPosition() to detect if the moved item is being moved to an open, pathable space. Though, I don't remember if you can pass an item to this function.
- It is possible to check for items/characters throughout the line of travel that the moved item will traverse, and one could modify how the item is moved based on this feedback. This could be done by iterating items/characters at remote points, offset in increments from the item location to its destination.
- Character movement can fail if it is blocked by other characters.
- Ice Walls can block movement but cannot be moved.
- Weights of items would probably need to be researched/changed to fit into a categorization as you've considered already; as you've said, it's certainly unreasonable to check against numerous roots/stats.
- A less impressive/less lore-friendly (but potentially easier) method of solving this problem would be to script in teleportation for these characters whenever they are stuck. FindValidPosition() would work for this almost entirely on its own.
Last edited by Ameranth; 13/02/17 07:13 PM.
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
Thanks for your response! 1) Have you ever successfully used 'FindValidPosition' in a script? It never did anything when I used it (it's supposed to work for items and characters). 2) I'll maybe check for characters close to the target position (though it's actually funny as it is). But I can't think of a way to check for walls without limiting the character too much with 'CanSee'. Actually the engine automatically checks for valid positions when NPCs move items but doesn't seem to check for walls. I'm not sure how much scripting effort I want to invest here, the results are overall okay and fit a 'goofy' game like DOS 3) Hm, characters would be a bit tricky. I can't let NPCs teleport or knock down every character who's possibly, just eventually in the way. Though it's not a viable 'cheese' tactic of players, is it? - Scriptwise, the character would try to move items, and end turn when there isn't left any. 4) I've added ice walls now (just checking for 200000 weight). Characters will attack them until they break. 5) I'll search through the object stats (and the game levels) in the next days, looking for lighter/heavier movable items and unmovable items within the defined weights. If you consider the final version of the script useful for your mod, feel free to use it
|
|
|
|
member
|
member
Joined: Dec 2016
|
1. I have, I use it in my AI for Phoenix Dive/Tactical Retreat/Misty Step as well as some other scripts. It's very poorly organized, so it's very easy to misuse. It uses the given Float3 both as input location and as its output location, and its "optional" parameter for character/item (the object whose AI bounds are used to calculate what constitutes a valid location) should probably be mandatory.
2. Yeah, probably not a huge worry if they're not killing one another with this behavior.
3. It's not usually something that players can exploit, but it certainly does occur. A great example is the Black Cove's first encounter, there is a small room off to the side where 4 skeletons will attack. If the player stands inside of the door, most of the skeletons will time-out without doing anything. There isn't a great way to solve this, though it would still be an improvement to have creatures immediately pass their turn rather than time-out for 30 seconds.
4. Nice, I'd like to see how you handled it.
5. Good luck :P
Thanks! I would definitely be interested. I have so little time these days though, I'm mostly trying to fix bug reports when I am not committed to other responsibilities.
Last edited by Ameranth; 13/02/17 10:05 PM.
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
1. Okay, I'll try that. Thanks for the tip! 3. I'll do some testings here. 4. Must test it first, which is more difficult than I thought. They always slip on the ice (they need better boots). 5. Very motivating I just noticed that unmovable items are the reason why characters get stuck in their reaction sometimes. The engine doesn't ignore CharacterMoveItem for unmovable objects and also doesn't interrupt the reaction. The only way to make the character react again is to interrupt the reaction manually or by certain actions (like requesting dialog, or by other reactions). That's not good... It's definitely necessary to exclude all of these items. A check for 'CanBeMoved' or 'CharacterCanMoveItem' (also checking required strength and telekinesis) would be very helpful. Edit: Maybe I can work with ItemIsMoving. Actually the better idea... Edit2: Even better: just setting an integer after CharacterMoveItem and checking it on a timer. If it isn't set it will interrupt the reaction.
Last edited by Abraxas*; 14/02/17 02:05 PM.
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
Currently investigating an issue where characters sometimes stop acting when the path is blocked. It's clear now that it happens in the 'GetClose' reaction, probably due to an unreachable target (the %TempTarget). Now actually this should cause OnMovementFailed and activate the 'MoveItems' reaction. But after some tests it seems there are objects that can block the path but don't cause an interruption!
Test:
1) I placed a double bed in my testlevel, generated Ai grid, and commanded several characters to a position right in the middle of the bed. They didn' move, as expected, and their reaction got interrupted (due to failed movement).
2) I placed a bridge tile (template: 1m_StoneWall_A_Bridge_Big_Floor_A_Item), generated Ai grid, and commanded them to the middle of this object. They DID move as far as they could and stopped when they were close to the object (the min and max range in 'CharacterMoveTo' was set to 0.1, so they were definitely too far away from the targeted position). But there was no interruption!
3) I placed several walls next to each other to get a big wall, generated Ai grid, and commanded them to the middle of this wall construction. They did the same as in 2). Moved as far as they could, stopped, but didn't throw an interruption event!
So after the already mentioned issue with unmovable items causing a character to not react any more (already solved in the script), this is a second source of problems. So I'll try to work with FindValidPosition to prevent characters from choosing targets they can't reach but don't cause an interruption (with the result of a character permanently trying to move to the target without interruption). Maybe path blocking characters are treated the same as objects in 2) and 3).
I tested this in the editor, so maybe, just maybe, things are different in the game. I hope they aren't. It already took me hours of pointless changes to the script.
|
|
|
|
member
|
member
Joined: Dec 2016
|
I had noticed that movement failed interrupts didn't always seem to work, but I was too frustrated with it to delve into it. Is it possible that, since the OnMovementFailed routine is part of a behavior, it is also being interrupted?
I think trying to make use of FindValidPosition is a good idea here, as OnMovementFailed seems inconsistent at best.
There is the potential to create your own OnMovementFailed interrupt, though; you could start a timer when ordering a movement behavior that checks the character's Float3 location. If the location is not changing, the character is not moving. The timer is stopped after the move is successful by placing StopTimer below the movement command in the script (or after a reasonable number of executions, to prevent timer buildup).
Last edited by Ameranth; 15/02/17 08:24 PM.
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
Is it possible that, since the OnMovementFailed routine is part of a behavior, it is also being interrupted? Not sure if I understand you right: Currently the missing interruption leads to a loop of the 'GetClose' reaction, it seems, with the character permanently trying to move to a target he cannot reach. Checking locations could be an option, but I think I'll try to use FindValidPosition first and recalculate a target position if it fails; I can use the _AttemptCounter to stop this behaviour after a few tries. Though, still assuming this is the actual (and only) problem here. As always, when trying to do something more advanced, issues arise. Quite impressing whenever a mod finally sees the light of the day (not to mention D:OS 1 itself).
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
Good progress. FindValidPosition helped to fix it (thanks for the INOUT hint, Ameranth, I hadn't noticed that). For the updated script see below. Changes: - fixed two problems with characters not reacting in combat (related to unmovable items and unreachable targets) - characters now attack ice walls if they block path - characters now attack the closest enemy when their path is blocked and they don't find items (not yet tested) - objects standing on other objects aren't regarded any more (doesn't work for all types of items other items can stand on) - re-organized the script to communicate with any combat or peace related script: the 'PathBlockAI' script must be included (#INCLUDE) in 'DefaultCharacter', the INTERRUPT code must be added to every reaction with movement to activate the AI for it (for combat it should be added to reactions of 'DefaultCharacter' and '_SKILLFROMITEM_Base' at least) Script:
INIT
CHARACTER:__Me
EXTERN FLOAT3:%TargetPos = null
FLOAT3:%TempTarget
INT:%ItemMoveSuccess = 0
BEHAVIOUR
REACTION GetClose, 0
USAGE PEACE
USAGE COMBAT
VARS
FLOAT:_distance
FLOAT:_MyX
FLOAT:_MyZ
FLOAT:_TargetX
FLOAT:_TargetZ
FLOAT:_AttemptCounter = 0
FLOAT:_AddX
FLOAT:_AddZ
FLOAT:_Divisor
FLOAT3:_Target
FLOAT3:_MyPos
FLOAT3:_TargetPosition
ACTIONS
Sleep(1)
Label("Again")
IF "c1&c2"
GetPosition(__Me,_MyPos)
GetVar(_TargetPosition,__Me,"TargetPos")
THEN
Set(%TempTarget,_TargetPosition)
ENDIF
IF "c1&c2&c3&c4&c5"
GetX(_MyPos,_MyX)
GetZ(_MyPos,_MyZ)
GetX(_TargetPosition,_TargetX)
GetZ(_TargetPosition,_TargetZ)
GetDistance(_distance,__Me,_TargetPosition)
THEN
Subtract(_TargetX,_MyX)
Subtract(_TargetZ,_MyZ)
GetRandom(_Divisor,1.8,1.9,2,2.1,2.2)
Divide(_TargetX,_Divisor)
Divide(_TargetZ,_Divisor)
Add(_MyX,_TargetX)
Add(_MyZ,_TargetZ)
SetX(%TempTarget,_MyX)
SetZ(%TempTarget,_MyZ)
CharacterLookAt(%TempTarget)
IF "c1"
IsGreaterThen(_AttemptCounter,4)
THEN
Set(_AttemptCounter,0)
SetPriority("MoveItems",0)
IF "c1"
IsInCombat(__Me)
THEN
CharacterEndTurn()
ELSE
DelayReaction("GetClose",10)
ENDIF
SetPriority("GetClose",0)
ELIF "c1"
IsLessThen(_distance,4)
THEN
CharacterLookAt(_TargetPosition)
IF "c1"
IsInCombat(__Me)
THEN
CharacterMoveTo(_TargetPosition,1,0,0,0.2,1)
ELSE
CharacterMoveTo(_TargetPosition,0,0,0,0.2,1)
ENDIF
SetPriority("GetClose",0)
ELIF "!c1"
IsLessThen(_distance,4)
THEN
IF "!c1"
FindValidPosition(%TempTarget,2.5,__Me)
THEN
Interrupt("GetClose")
ENDIF
IF "c1"
IsInCombat(__Me)
THEN
CharacterMoveTo(%TempTarget,1,0,0,0.2,1.4)
ELSE
CharacterMoveTo(%TempTarget,0,0,0,0.2,1.4)
ENDIF
SetPriority("MoveItems",0)
ELSE
Set(_AttemptCounter,0)
SetPriority("MoveItems",0)
SetPriority("GetClose",0)
ENDIF
ENDIF
INTERRUPT
Reset()
ON
OnMovementFailed(_)
OnManualInterrupt("GetClose")
ACTIONS
Add(_AttemptCounter,1)
SetPriority("MoveItems",3000)
SetPriority("GetClose",0)
REACTION MoveItems, 0
USAGE PEACE
USAGE COMBAT
VARS
ITEM:_Item
ITEM:_Item2
ITEM:_IgnoreItem1
ITEM:_IgnoreItem2
ITEM:_IgnoreItem3
FLOAT:_Weight
FLOAT:_X
FLOAT:_Z
FLOAT:_MyPosX
FLOAT:_MyPosZ
FLOAT3:_MyPos
CHARACTER:_Enemy
CHECK "(((c1&c2&c3&c4&((!c5&c6)|c7))|(c8&c9&c10&((!c11&c12)|c13)))&!c14&!(c15&c16))|(c17&c18)"
ItemGet(_Item,__Me,6,Random,Distance,"")
IsFacing(__Me,_Item)
CanSee(__Me,_Item)
ItemGetStat(_Weight,_Item,Weight)
IsLessThen(_Weight,1000)
IsLessThen(_Weight,40001)
IsEqual(_Weight,200000) //Ice_Crystal
ItemGet(_Item,%TempTarget,6,Random,Distance,"")
IsFacing(__Me,_Item)
ItemGetStat(_Weight,_Item,Weight)
IsLessThen(_Weight,1000)
IsLessThen(_Weight,40001)
IsEqual(_Weight,200000) //Ice_Crystal
ItemIsDestroyed(_Item)
ItemGet(_Item2,_Item,2,Random,Distance,"")
IsObjectOnObject(_Item,_Item2)
IsInCombat(__Me)
CharacterGet(_Enemy,__Me,5,Lowest,Distance,Enemy)
ACTIONS
IF "!c1"
IsEqual(_Enemy,null)
THEN
CharacterMoveTo(_Enemy,1)
CharacterAttack(_Enemy)
Goto("End")
ENDIF
IF "c1"
IsInCombat(__Me)
THEN
CharacterMoveTo(_Item,1)
ELSE
CharacterMoveTo(_Item)
ENDIF
IF "c1"
IsEqual(_Weight,200000) //Ice_Crystal
THEN
WHILE "!c1"
ItemIsDestroyed(_Item)
DO
CharacterAttack(_Item)
ENDWHILE
ELSE
GetPosition(__Me,_MyPos)
IF "!c1&c2"
ItemIsDestroyed(_Item)
FindValidPosition(_MyPos,5,_Item)
THEN
StartTimer("CheckMoveItemSuccess",3,0)
CharacterMoveItem(_Item,_MyPos)
Set(%ItemMoveSuccess,1)
ENDIF
ENDIF
Label("End")
Set(_Enemy,null)
SetPriority("MoveItems",0)
INTERRUPT
Reset()
Set(_Enemy,null)
SetPriority("MoveItems",0)
EVENTS
EVENT CheckItemMoveSuccess
ON
OnTimer("CheckMoveItemSuccess")
ACTIONS
IF "c1"
IsEqual(%ItemMoveSuccess,1)
THEN
Set(%ItemMoveSuccess,0)
ELSE
Interrupt("MoveItems")
ENDIF
EVENT GetCloseActivation
ON
OnCharacterEvent(__Me,"ActivateGetClose")
ACTIONS
SetPriority("GetClose",2000)
INTERRUPT code:
//to be added to every reaction with movement
VARS
FLOAT3:_Pos
INTERRUPT
//Reset() //not necessary for functionality, but for most reactions recommended
ON
OnMovementFailed(_Pos)
ACTIONS
SetVar(__Me,"TargetPos",_Pos)
CharacterEvent(__Me,"ActivateGetClose")
Some notes: - sometimes characters won't act for a few seconds if they fail to find a solution; though they'll skip turn after a while (combat) or delay the reaction (peace) - characters can't find path blocking items if they aren't facing the position (the closer to the target the more likely this happens); increasing the angle too much would make the script more unprecise - it's still unclear under which conditions a failing movement doesn't cause an interruption; as long as this happens in the 'GetClose' reaction, it will just lead to a character skipping turn if he doesn't find another solution (or delaying the reaction); if it happens in any other peace or combat reaction this will lead to a character not acting at all (peace) or for a longer while (combat, assumed that the 'DelayReaction' call in the reactions of 'DefaultCharacter' or '_SKILLFROMITEM_Base' was removed - I always do that, since DelayReaction seems to be broken in combat, not counting seconds but eternities) - characters still aren't super-intelligent, but they're definitely not helpless any more
|
|
|
|
member
|
member
Joined: Dec 2016
|
I'm glad you're making such progress! This is very promising stuff indeed.
- Would it makes sense to have characters turn to face the target before ever attempting a move? This would (usually) ensure facing is correct.
- I didn't know that DelayReaction didn't consider seconds while in combat... I'll have to remember this.
- My only criticism at a glance is the very large complex conditional for MoveItems' Check critera (performance concern), but I think this behavior never maintains a priority for very long, correct?
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
1) That's already the case. But the line of sight between the character and the target (the shortest path) can be completely different from the actual path, mostly when buildings come into play (with a blocked door). But it doesn't happen very often, and is not easy to exploit.
3) I also feel a bit uncomfortable about this, but this reaction is only active on demand for a few seconds and, of course, only for one single character. Though it would be useful to measure the influence of this reaction on performance. Epic Encounters is quite scripting-heavy, so it's a reasonable concern.
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
Update: - fixed an issue where characters repeatedly moved items to the exact same position when their own position didn't change (FindValidPosition always gives the same output if nothing changed) - improved decisionmaking in finding items to move (the closest item will be chosen with a 40% chance, since it's often the best solution) - removed IsObjectOnObject check Script:
INIT
CHARACTER:__Me
EXTERN FLOAT3:%TargetPos = null
FLOAT3:%TempTarget
INT:%ItemMoveSuccess = 0
BEHAVIOUR
REACTION GetClose, 0
USAGE PEACE
USAGE COMBAT
VARS
FLOAT:_distance
FLOAT:_MyX
FLOAT:_MyZ
FLOAT:_TargetX
FLOAT:_TargetZ
FLOAT:_AttemptCounter = 0
FLOAT:_AddX
FLOAT:_AddZ
FLOAT:_Divisor
FLOAT3:_Target
FLOAT3:_MyPos
FLOAT3:_TargetPosition
ACTIONS
IF "c1&c2"
GetPosition(__Me,_MyPos)
GetVar(_TargetPosition,__Me,"TargetPos")
THEN
Set(%TempTarget,_TargetPosition)
ENDIF
IF "c1&c2&c3&c4&c5"
GetX(_MyPos,_MyX)
GetZ(_MyPos,_MyZ)
GetX(_TargetPosition,_TargetX)
GetZ(_TargetPosition,_TargetZ)
GetDistance(_distance,__Me,_TargetPosition)
THEN
Subtract(_TargetX,_MyX)
Subtract(_TargetZ,_MyZ)
GetRandom(_Divisor,1.8,1.9,2,2.1,2.2)
Divide(_TargetX,_Divisor)
Divide(_TargetZ,_Divisor)
Add(_MyX,_TargetX)
Add(_MyZ,_TargetZ)
SetX(%TempTarget,_MyX)
SetZ(%TempTarget,_MyZ)
CharacterLookAt(%TempTarget)
IF "c1"
IsGreaterThen(_AttemptCounter,4)
THEN
Set(_AttemptCounter,0)
SetPriority("MoveItems",0)
IF "c1"
IsInCombat(__Me)
THEN
CharacterEndTurn()
ELSE
DelayReaction("GetClose",5)
ENDIF
SetPriority("GetClose",0)
ELIF "c1"
IsLessThen(_distance,4)
THEN
CharacterLookAt(_TargetPosition)
IF "c1"
IsInCombat(__Me)
THEN
CharacterMoveTo(_TargetPosition,1,0,0,0.2,1)
ELSE
CharacterMoveTo(_TargetPosition,0,0,0,0.2,1)
ENDIF
SetPriority("GetClose",0)
ELIF "!c1"
IsLessThen(_distance,4)
THEN
IF "!c1"
FindValidPosition(%TempTarget,4,__Me)
THEN
Interrupt("GetClose")
ENDIF
IF "c1"
IsInCombat(__Me)
THEN
CharacterMoveTo(%TempTarget,1,0,0,0.2,1.4)
ELSE
CharacterMoveTo(%TempTarget,0,0,0,0.2,1.4)
ENDIF
SetPriority("MoveItems",0)
ELSE
Set(_AttemptCounter,0)
SetPriority("MoveItems",0)
SetPriority("GetClose",0)
ENDIF
ENDIF
INTERRUPT
Reset()
ON
OnMovementFailed(_)
OnManualInterrupt("GetClose")
ACTIONS
Add(_AttemptCounter,1)
SetPriority("MoveItems",3000)
SetPriority("GetClose",0)
REACTION MoveItems, 0
USAGE PEACE
USAGE COMBAT
VARS
ITEM:_Item
ITEM:_Item2
ITEM:_IgnoreItem1
ITEM:_IgnoreItem2
ITEM:_IgnoreItem3
FLOAT:_Weight
FLOAT:_X
FLOAT:_Z
FLOAT:_MyPosX
FLOAT:_MyPosZ
FLOAT3:_MyPos
CHARACTER:_Enemy
CHECK "(((((c1&c2)|(c3&c4))&c5&c6&((!c7&c8)|c9))|(c10&c11&c12&((!c13&c14)|c15)))&!c16)|(c17&c18)"
IsRandom(0.4)
ItemGet(_Item,__Me,6,Lowest,Distance,"")
ItemGet(_Item,__Me,6,Random,Distance,"")
CanSee(__Me,_Item)
IsFacing(__Me,_Item)
ItemGetStat(_Weight,_Item,Weight)
IsLessThen(_Weight,1000)
IsLessThen(_Weight,40001)
IsEqual(_Weight,200000) //Ice_Crystal
ItemGet(_Item,%TempTarget,6,Random,Distance,"")
IsFacing(__Me,_Item)
ItemGetStat(_Weight,_Item,Weight)
IsLessThen(_Weight,1000)
IsLessThen(_Weight,40001)
IsEqual(_Weight,200000) //Ice_Crystal
ItemIsDestroyed(_Item)
IsInCombat(__Me)
CharacterGet(_Enemy,__Me,5,Lowest,Distance,Enemy)
ACTIONS
IF "!c1"
IsEqual(_Enemy,null)
THEN
CharacterMoveTo(_Enemy,1)
CharacterAttack(_Enemy)
Goto("End")
ENDIF
IF "c1"
IsInCombat(__Me)
THEN
CharacterMoveTo(_Item,1)
ELSE
CharacterMoveTo(_Item)
ENDIF
IF "c1"
IsEqual(_Weight,200000) //Ice_Crystal
THEN
WHILE "!c1"
ItemIsDestroyed(_Item)
DO
CharacterAttack(_Item)
ENDWHILE
ELSE
GetPosition(__Me,_MyPos)
GetRandom(_X,-3,-2,2,3)
GetRandom(_Z,-3,-2,2,3)
IF "!c1&c2&c3"
ItemIsDestroyed(_Item)
GetX(_MyPos,_MyPosX)
GetZ(_MyPos,_MyPosZ)
THEN
Subtract(_MyPosX,_X)
Subtract(_MyPosZ,_Z)
SetX(_MyPos,_MyPosX)
SetZ(_MyPos,_MyPosZ)
IF "c1"
FindValidPosition(_MyPos,4,_Item)
THEN
StartTimer("CheckMoveItemSuccess",3,0)
CharacterMoveItem(_Item,_MyPos)
Set(%ItemMoveSuccess,1)
ENDIF
ENDIF
ENDIF
Label("End")
Set(_Enemy,null)
SetPriority("MoveItems",0)
INTERRUPT
Reset()
Set(_Enemy,null)
SetPriority("MoveItems",0)
EVENTS
EVENT CheckItemMoveSuccess
ON
OnTimer("CheckMoveItemSuccess")
ACTIONS
IF "c1"
IsEqual(%ItemMoveSuccess,1)
THEN
Set(%ItemMoveSuccess,0)
ELSE
Interrupt("MoveItems")
ENDIF
EVENT GetCloseActivation
ON
OnCharacterEvent(__Me,"ActivateGetClose")
ACTIONS
SetPriority("GetClose",2000)
INTERRUPT code:
//to be added to every reaction with movement
VARS
FLOAT3:_Pos
INTERRUPT
//Reset() //not necessary for functionality, but for most reactions recommended
ON
OnMovementFailed(_Pos)
ACTIONS
SetVar(__Me,"TargetPos",_Pos)
CharacterEvent(__Me,"ActivateGetClose")
Last edited by Abraxas*; 22/02/17 10:39 AM. Reason: Little correction in FindValidPosition; removed Sleep(1)
|
|
|
|
member
|
member
Joined: Dec 2016
|
Very nice progress, I'll probably add this into EE for internal testing after I send out the next patch as well (assuming you're still okay with that).
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
I'm completely okay with that (I'm not even sure if I'd had started another attempt on this without your mod, at least not immediately). A test would be very helpful, especially in regard to finetuning (wherever possible).
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
I rechecked the behaviour of DelayReaction in combat: I'd highly recommend to disable it in the relevant reactions (it's always used OnMovementFailed in several reactions of combat scripts). It always delays the reaction for the whole turn (at least), regardless of the entered seconds. But 3 'seconds' or 200 'seconds' seems to make a difference, though I don't know what or when the engine counts here (during WAITING?). It doesn't count turns instead of seconds, that's for sure. For my scripts to work properly, the reactions of combat scripts must not be delayed for the whole turn but stay active. There's no fallback then, but my script contains a rule for ending the turn when failed movement interrupted those reactions (--> four attempts to find a solution).
|
|
|
|
member
|
member
Joined: Dec 2016
|
Elric will be testing with your scripting once I have this next patch uploaded, then The base game's AI has proven to cause enemies' turns to time-out relatively often, so this will be wonderful in addressing even just that. Good to know about DelayReaction; I didn't use it too much because it always felt broken, but I'll keep in mind that it really is broken moving forward.
|
|
|
|
addict
|
OP
addict
Joined: Sep 2015
|
The base game's AI has proven to cause enemies' turns to time-out relatively often, so this will be wonderful in addressing even just that.
If it's related to failing movement, it will probably help. Elric will be testing with your scripting once I have this next patch uploaded, then All right. He'll probably choose other test situations than I did. Will human or AI succeed? I've made one little change to the script above. Just an adjustment in FindValidPosition for the temporary target (from 2.5 range to 4). I'm uncertain if I should set the priorities of 'GetClose' and 'MoveItems' to 0 OnTurn. Not sure if it's needed to prevent characters from continuing actions they couldn't finish on their last turn (due to missing action points) although the situation was already solved by another character. And it would ensure that there's never an issue with unintentionally remaining priorities. If Elric observes behaviour of this kind, it should be added.
|
|
|
|
member
|
member
Joined: Dec 2016
|
Hey Abraxas, since I'm lazy and a noob (been busy lately), would you mind uploading the relevant scripts from Epic Encounters with your changes so that I can just paste them in for Elric and have him start testing?
<3 u
Last edited by Ameranth; 22/02/17 06:10 AM.
|
|
|
Moderated by Bvs, ForkTong, gbnf, Issh, Kurnster, Larian_QA, LarSeb, Lar_q, Lynn, Monodon, Raze, Stephen_Larian
|
|