Trng Plugin SDK 1


Summary

Introduction
Who is able to create plugins?
          Who are you?
          Different levels of knowledge
Installing Microsoft Visual Express 2010
          How to prepare the Level for this Tutorial
                    Loading the starting plugin project
                    Copy the starting Script files in NG_Center
                    Copy the wad files in TRLE folder
                    Copy the title.tr4 file to TRLE folder
                    Copy the project files to TRLE folder
                    Copy the image file to TRLE folder
                    Build all
Gaining confidence at our plugin source
          Meaning of different source files
Kick-Off
          Learning to move in the Editor of Visual Express 2010
          cbCycleBegin() function
                    What is a callback?
Introduction to Plugin Exercises
Exercise 1: the Magic Flare
          Commands and Functions
          Briefing about C++ syntax
          Bracket parenthesis
          What is Get() function?
          What is enumGET ?
          Our first special effect
          Building the project
          Copy the plugin .dll in trle folder
          What will it happen in game?
          A bit better but ...
          Reading Game Command Input
          Structures and testing Flags
          Dot or arrow?
          What is the operator: "&" ?
          What are "+=" and "-=" operators?
          What is the reason of that spaces at left of instructions?
          The rule to choose how many tabulations insert
          How to add to Visual Express some buttons to help you in editing
          Complete our Magic Flare effect
          Our first function
          Moving down Lara
          Our first global Variable
          MyData global structure
          FlareVSpeed or VSpeedFlare ?
          Using a global variable
          The "++" or "--" operators
          Our final Code
          Well Done
          How to handle hardcoded traples
          Dangeorus Walls
          Final Dangerous Walls code
          Testing Dangerous Walls
          Conditional Operators
          Coming up to the second floor
          The Teeth-Spikes on the ceiling
          Ceiling Teeth-Spikes
          Discover the Collision Box of Moveable Items
          Finding Items - The Find() function
          Returned values from Find() function
          Working with Vectors
          How to parse Vectors: the "for" statement
          Using TRNG triggers from our code
          Final code of CeilingTeethSpikes
          Testing CeilingTeethSpikes()
          Homeworks
Exercise 2: Lost in Space
          How to create a new Trigger
          The TRG file
          Description of the trigger
          Sections of TRG file
          Our first flipeffect
          How to declare the Arguments used by our Trigger
          The declaration of our Flipeffect 800
          How to create auto-list of arguments. The #REPEAT# tag
          Our trigger in NGLE program
          How to catch the activation of our triggers in game
          Moving the Cube
          To jerk or not to jerk?
          Verification of jerky movement of the Cube
          How to get a gradual movement
          The Progressive Actions
          Get a new Progressive Action
          The structure of Progressive Actions
          Declare new AXN value
          The chameleonic Structure for Progressive Actions
          The real declaration of StrProgressiveAction structure
          To start our progressive action to move the cube
          The PerformMyProgrAction() function
          The final code for AXN_MOVE_CUBE progressive action
          Improvement of the Cube movements: detection of room collisions
          How to detect the height of the floor
          The FLOOR global structure
          bool TestFullWall
          Slope types for Floor
          The new code to detect floor height
          Not so fine, this time
          Robotic Random Movemement
          Our new function with arguments
          Function to detect "Free Way" for our objects
          The IsFreeWay() function
          How to work with directions
          The GetIncrements() function
          Arguments of Input or of Output?
          The code of IsFreeWay() function
          How to use IsFreeWay() function
          Our new robotic Cube code
          Moving on the slopes
          How to change the IsFreeWay() function to accept slope sectors
          Changing the code of IsFreeWay() function to manage slopes
          It works but there are problems...
          How to align the Vertical Orienting with Slope
          Another bug to fix: rise or declivity?
          Modify code in flipeffect 800
          Modify code in progressive action
          The code of progressive action after last changes
          Last bug to fix
          How to compute the size of a Moveable Item
          The code of GetItemSize() function
          Change to the IsFreeWay() function to work with any moveable
          Final result of our Robotic Cube
          Homeworks
                    Homework 1
                    Homework 2
Exercise 3: The Cleaner Robot
          The Slot structure
          The management Procedures of moveable Items
                    The Control() procedure
                    The Initialise() procedure
                    The Collision() procedure
                    The Floor() and Ceiling() procedures
                    The Draw() procedure
                    The DrawExtras() procedure
          How to change Management Procedure of Slot
                    The cbInitObjects() callback
          What does it happen when we use an Unmanaged Enemy?
          Initialise the Cleaner Robot
                    How to check if a flag is missing
                    The Pointer to function
          How to initialise the item structure of our Cleaner Robot
                    How to set the FITEM_ flags for FlagsMain field of structure item
          How to set the Collision Management Procedure
                    TriggerActive() tomb4 function
                    GetMaxDistance() function
                    TestBoundCollide() tomb4 function
                    TestCollision() tomb4 function
                    ItemPushLara() tomb4 function
          Summary of current Robot Cleaner Code
          We stopped lara, finally
          That weird animation for Lara collision
          The Control() procedure for our Robot Cleaner
          AnimateItem() tomb4 function
          To do move the Robot Cleaner
          Customizable variables of Item Structure
          Erratum about old code for robotic movement
          Summay of changes about variables from old code to new code
          New release of Robotic Movement code
          The limits of moveable collision boxes
          How to fix the bug about alignment with floor rail-way
          Improvement of IsFreeWay() function
          Change the calls for new IsFreeWay() function
          Kill Lara on contact
          How to stop the movement of the Robot Cleaner
          How to add special effects to the killing of Lara
          How to add sparks effect
                    The TriggerFlareSparks() tomb4 function
                    The GetJointAbsPosition() tomb4 function
          How to add an effect at given mesh
          The function AddSparksEffect() function
          It doesn't work fine
          The CheckFloor() function in depth
          Another progressive action: AXN_ADD_EFFECT
          The changes at AddSparksEffect() function
          The code of AXN_ADD_EFFECT progressive action
          It has been increased the number of sparks but ...
          How to find the borders of current sector
          The new AddSparksToCables() function
          The GetRandomControl() tomb4 function
          Ok, the sparks effect sucks...
          How to have selective collisions
          Ok, from back it's salubrious
          How to detect collision between enemies
          How to do Robot killed enemies
          Robot cleaner: a kill machine
          How to turn gradually the direction
          Restyle the ControlRobotCleaner() function creating sub-functions
          Manage the horizontal turning: the RobotCleanerHTurning() function
          Manage the movement and check for free direction: the RobotCheckDirections() function
          Chaning position of the robot and check for the vertical turning: the RobotMoveAndVturning() function
          Adding special effects to the robot: the RobotAddEffects() function
          Detect collision with enemies: the RobotCheckEnemyCollisions() function
          The new layout of ControlRobotCleaner() function
          Final testing of Cleaner Robot
                    Homeworks
Exercise 4: Star Wars Robot
          Set basic code for Star Wars Robot
                    Code to initialise slot of Star Wars Robot
                    The InitialiseRobotStarWars() function
                    The CollisionRobotStarWars() function
                    The ControlRobotStarWars() function
          Animations of Star Wars Robot
          The abilitites of Star Wars Robot
          AI behaviour of StarWars Robot
          Meaning of State-IDs
          Moving the Star Wars Robot
                    Short description of first release of Control() function
          Turn the feet on the slope
          Code to discover the sector type in according with direction of the robot
                    The DiscoverSectorType() function
          How to monitor current animation
                    The GetRelativeAnimation() function
          The code to turn the Robot's feet
          How to detect the collision with static items
                    The DetectCollisionWithStatics() function
          How to detect box (gray) sectors
          How to read OCB settings
          How to detect the presence of Lara
                    The CheckDirection() function
          The code to check if the robot is able to see Lara
          How to create our customize script commands
          The plugin.script file
          Our plugin_trng.script file
          How to read our Customize command data
          The GET_MY_CUSTOMIZE_COMMAND parameter
          How to discover the ngle index of a moveable
          How to discover if Get() function failed
          Reading settings for our Star Wars Robot
          How to compute facing of the head of Robot
          How to discover the facing of a single mesh of moveables
          The FindHeadFacing() function
          Testing new detection code
          AI Features of Star Wars Robot: the Supervisory Skills
                    When perform the check about looking around?
          Different code in according with current State-id
          The IsMissingWall() function
          The different heigths of Robot's head
          How to discover the size of object in game units
          How to improve code planning whereby distinct functions
          Restyle for Star Wars Robot Code
                    ControlRobotStarWars() function
                    RobotSW_ReadSettings() function
                    RobotSW_DetectAndAttack() function
                    RobotSW_MoveAndSteer() function
                    RobotSW_TurnFeet() function
          Management of "looking around" phase
          The RobotSW_LookAround() function
          Debugging the Looking Around feature
          How to use the Debugger of Visual Express 10
          New SW Robot code after debugger fixing
                    Created new function: the SetAnimationAndSpeed() function
                    Create new mnemonic constat: LOOKSW_ENDED_LEFT_TRY_RIGHT
                    Code of RobotSW_DetectAndAttack() function after bug fixing
                    New code of RobotSW_LookAround() function after bug fixing
          How to improve IsFreeWay() function to support all moveables as obstacles
          Improving detecting mode
          How to remember what we did a moment ago?
          The Reserved_36 field to remember previous looking operations
          Constants to remember previous looking operation
          The Reserved_38 field to save current looking operation
          The new detection code to avoid repetitions
          What's the news in RobotSW_LookAround() function?
                    The Reserved_36 field
                    The Reserved_38 field
          Other improvements for Star Wars Robot
                    The new SWR_DISABLE_INSPECTION flag
                    Testing the SWR_DISABLE_INSPECTION flag
          Adding a shadow for the robot
          Shadown doesn't work fine
          Adding injuring on touch
                    The code to injury lara on touch
                    Those weird error messages about "identifier not found"
                    It works, perhaps a bit too much...
          How to get settings from Enemy script command
          How to shoot electrical lightnings
                    The TriggerLightning() function
          Why should we reinvent the wheel?
          How to handle triggers requiring Param commands in the script
          How to create new script commands
          The code to call the ShootLightning() fucntion
          How to handle delay times
          How to handle looped sound effects
          Input arguments of SoundEffect() function
          Trying in game new shooting skill
          Get customizable also damage2 for Star Wars Robot
          Final release of ShootLightning() function
          Homworks
          Changing the target point where lara will be hit.
                    The CreateAIObject() function
                    How to delete an AI item that we created
                    Where does it save the OcbValue?
          Shooting only when Robot is head on Lara
Exercise 5: The Swinging Crane
          Setting default procedures for Crane object
          The AssignSlot command to support new Objects
          How to manage new OBJ_ mnemonic constants
          How to read from code the data of AssignSlot commands
          Setting default procedures for Crane object
                    The Initialise() function
                    The Collision() function
                    The Control() function
          Initialise Slot structure for Crane items
          Typing the basic code for our new Object
                    Code for InitSlotCrane() function
          The default draw procedure
                    Code for CollisionCrane() function
                    Code for InitialiseCrane() function
          How to plan the Control() code
          Differences between other robots and the Crane
          Reserved variables used by Swinging Crane
          Code for ControlCrane() function
          Code for ControlCraneAutomatic() function
          How to move the crane to get it near to Lara
          The code for Crane_SetNewDirection() function
          Code for IsValidCeiling() function
          Testing in game the horizontal movements of the Crane
          Code for Crane_MoveUpDown() function
          Testing falling down of the Crane
          The animations of Swinging Crane
          Change the code to engage animations
          Crane bites Lara
          How to add sound effects to our new Objects
          How to get the chance to customize the sounds for our new objects
          Adding sounds to Swinging Crane
          The hardcoded sound effects in our code
          The customize command for our sound effects
          How to read data from Customize=CUST_SFX_DEMO command
          The GetSFX() function to read our customized sound effeects
          What is there upstairs?
          Testing the disappearing of crane Pole
          To put a shadow or less?
          A weird shadow
          How to create a proportional Shadow
          The new proportional shadow of the Crane
          Setting for proportional shadow
          Homeworks
                    Adding a sound for change of direction
          Biting really Lara and taking her away
                    Where
                    When
                    How
                    The End
Exercise 6: Driving the Crane
          The Control Panel of the Crane
          How to initialise the control panel object
          The default procedures used by Animating objects
          The collision procedure for Control Panel of the Crane
          Detect right position of Lara
          How to discover the TestPosition data for right alignment with the control panel
          The CheckPositionAlignment() function
          How to engage the Crane
          How to enable a vehicle
                    VehicleIndex
          The Crane_EngageDriving() function
          The frozen Lara
          How to detect the activation of the Crane
          How to animate lara during a vechicle phase
          The game commands to drive the crane
          Global Variables used by drivable Crane
          How to handle horizontal movements of the Crane
          The IsCraneFreeDirection() function
          How to set a new global trigger event
          How to engage in the code a GT_ event
          Our code to signal the begin of driving mode
          A bit of scripting to have right camera view for the Crane
          The new Crane Camera
          Moving vertically the Crane
          A flow-chart to plan the vertical movements of the Crane
          The code to manage the vertical movements of the Crane
          The State-IDs for drivable Crane
          The code of ControlCraneManual() function
          Testing up/down movements of the Crane
          How to recognize and move different items using our Crane
          The IsCollidingWithSomeItem() function
          The code to grab items
          The Crane_MoveCraneVertically() function to detect collisions
                    When the crane is empty....
          The ActionForObject() function
          The ItemPushAwayItem() function
          Moving the grabbed item
          The changes in ControlCraneManual() function to grab items
          The Crane_WaitEndEngageAnimation() function
          The Crane_InputVerticalMov() function
          The Crane_InputHorizontalMov() function
          The Crane_InputOthers() function
          The Crane_UpdateAnimations() function
          The Crane_MoveCraneHorizontally() function
          Testing of grabbing feature
                    The Big Pillar is good
                    The crash of Shater Vase is good
                    The jeep grabbing seems fine
                    The stop for collision with Grave Chest is good
                    The grabbing of rollingball is not so good
                    A disaster when we try to grab the jeep from misaligned sector
          Improving the grabbing phase
          Code to have deep grabbing
                    The changes in Crane_MoveCraneVertically() function
                    The changes in Crane_UpdateAnimations() funcion
                    Changes in ControlCraneManual() function
          Testing new deep grabbing
          Easy grabbing or hard grabbing?
          The hard grabbing mode
          The code to handle hard grabbing
          The changes in Crane_MoveCraneVertically() function for hard grabbing
          The changes in Crane_InputOthers() function for hard grabbing
          The changes in Crane_UpdateAnimations() function for hard grabbing
          Testing new hard grabbing
          Improve the "release item" phase
          How to manage a parallel process
          How to initialise a new progressive action
          The progressive action to do falling down items
          The CreateFallingDownForItem() function
          The code for AXN_FALLING_DOWN_ITEM progressive action
          The changes in ActionForObject() function
          The new code for ActionForObject() function
          Changes in Crane_MoveCraneVertically() function
          The changes in Crane_InputOthers() function
          Testing the code for falling down of the grabbed Item
          What yet it doesn't work
          Adding splash and ripples on the water surface
          Testing splash feature
          Changes in Crane_MoveCraneVertically() function to do splash and ripples for the crane
          Testing Splash and Ripples for the Crane
          Other fixing for the crane
          The AlignLaraAtPosition() function
          Changes in CollisionCtrlPanelCrane() function to support Lara's alingment
          Testing self-alignment of Lara with Control Panel
          Failure of AlignLaraAtPosition() function
          To set a vertical limit for Crane
          How to handle a numeric value in ocb field
          Using a bit mask to work only on some bit group
          Why to do binary shift?
          The GetCraneOcbVerticalLimit() function
          How to manage the vertical limit for the crane
          Changes in Crane_MoveCraneVertically() function to support max vertical limit
          The new Crane_UpdateAnimations() function
          Testing new vertical limit
          Updating also automatic crane with vertical limit
          Testing max vertical limit on Automatic Crane
          Another bug to fix about automatic grabbing
          Changes in Crane_MoveCraneVertically() function to fix automatic grabbing bug
          Changes in Crane_InputOthers() function to fix automatic grabbing bug
          Testing the fixing for automatic grabbing
          Houston, we've had a problem
          The StartMoveItem() and EndMoveItem() functions
          The collision box of the Crane
          How to fix the bug about grabbing of the pushable item
          Changes in CreateFallingDownForItem() function
          Remember to call also the EndMoveItem() function at end
          Testing the fixing for pushable item
          That weird collision box of pushable items
          Solved another bug about pushable items
          Saving the game in the middle of pushable movements
          The changes for crane management of pushable item
          The changes for pushable item management from progressive action
          Fixing the landing of the grabbed item
          Changing the collision of crane when it is drivable
          Testing new crane collisions
          Problems with rollingball item
          Testing multi grabbing for rolling ball
          Alignmment of position of the rolling ball
          Type the description of OCB codes in our .ocb file
                    The .ocb file of our plugin
          Description for Control Panel OCB codes
          Homeworks for drivable Crane
                    Set new ways to interact with other special moveables
                    To improve the sound effects
Exercise 7: The Mechwarrior
          The get-in animation
          Fixing the collision box for get-in animation
          The standard procedures for Mechwarrior
          News about slot assignment and WadMerger
          Initialise the Mechwarrior slot
          Initialise Lara's animation slot of Mechwarrior
          The InitialiseMech() function
          The CollisionMech() function
          The ControlMech() function
          Discover ideal position for get-in animation on Mechwarrior
          The CollisionMech() function
          The Mech_GetIn() function
          The Mech_SetAnimations() function
          The Mech_NormalizeCoordinate() function
          Testing get-in animation for Mechwarrior
          The code for get-off animation of Mechwarrior
                    The ControlMech() function
                    The Mech_InputCommands() function
                    The Mech_UpdateAnimations() function
                    The Mech_QuitDriving() function
          Testing the get-off animation for Mechwarrior
          How to get a set position animcommand directly whereby code
          Testing fixing for get-off animation
          How to handle camera mode for get-in animation
          The SetCamera() function
          New camera setting for get-in Animation
          Set permanently the new camera mode
          The camera mode for get-off animation
          Animations to move forward
          The computation about collision for big objects
          The constants to identify collision type
          The Mech_CheckRoomCollisions() function
          The code to do move forward the Mechwarrior
          The State Id change records of animations
          Changes in Mech_InputCommands() function
          New ControlMech() function
          The new Mech_UpdateAnimations() function
          Testing the forward movement
          Improving the management of UP command
          Extending the control for collisions
          All Animations of Mechwarrior
          Planning the future improvements for Mechwarrior code
          How avoid the violation of collisions in the swap animations
          How to handle the floor collisions different than CRC_OK and CRC_STOP
          Next floor or current floor?
          Checking if a given input command is acceptable
          How to handle the collision check, when the mechwarrior is going at reverse
          Final code of Mech Warrior
          Final Input Commands for Mech Warrior
          Homeworks for Mech Warrior
                    To handle special moves for kicking and beating
                    Manage collision with enemies or statics
                    What will it happen when Mech is "swimming" in the water, quick sands or lava rooms?
                    Will be able Shooting enemies to injury Lara?
                    Shooting skill for Mech Warrior
          Reference to external Help Files
Table of Contents





Introduction

Plugin project is a SDK (Software Development Kit) to create a .dll (Dynamic Link Library), like Tomb_NextGeneration.dll, with new features and skill to improve tomb raider 4 engine.

You'll be able to create new triggers (action, flipeffect or condition triggers), new script commands and new external programs to work with NG_Center program, too.
Your plugin will be able also to use the features already present in tomb_nextgeneration.dll, append new features or replace them.




Who is able to create plugins?

Theoretically all people having experience in trng scripting, in particular way if you did experience with trng variables and advanced scripting, will be able to create their own plugin.
In spite that, to create a dll (dynamic link librayr) it's necessary having programming knowledges, in the case of trng plugin you'll have already all sources to create your library (the plugin sources are already a complete working project), for this reason you have only to type some commands in given sections of these sources to add your new features to plugin and therefor also to trng.

In this help file we'll learn how to do this.

Who are you?

In according with your knowledge about game playing, scripting or programming there will be different pages of this help, and sections of code, you could study and to use.
In trng sdk we will use three icons to identify the different level of approach and knowledge for that chapter or tool.

Level Builders. The image at left means that the section is suggested for level builders.
The level builders having already a good knowledge about scripting, variables, global triggers and conditional trigger, proabably will be favorites to understand better these topics. Anyway the macro commands made for level builder coding are very alike than script command in NG_Center and for this reason all level builders will be able to add their own skills with trng plugins.


Programmers. The image at left means that the section will be only for programmers. If you have no knowledge about programming you'll have no chance to understand these topics, therefor skip them and don't worry. The 90% of new skills that it's possible adding to trng, it's possible make it using commands and knowledge for level builders.



Technical Infos - Intermediate level. The image at left introduces a technical description of some characteristic of trng, tomb4 engine or plugin that is probably not necessary for level builders but it could be useful in some circustances. Pratically this means that, if you are a level builder, you can skip these sections or you can study them in the case you are interested to approach programming world beginning to understand better some technical knowledges.

Note: in the case you are a programmer you should find useful all above sections, since also sections for level builder contain many stuff and basic knowledge useful for your advanced targets. In this meaning, the icons will be used only to warn level builders when the section is suggested for them or less.
In most of cases I'll use only one icon to describe the level of some topic, the only exception is when I introduce a link to another page where there could be topic of different levels. In that case you could see two or more icons at left of that link and it will mean: in the page you'll load with this link you'll find topic of levels as described from two/three icons.
About the cases where there is no icon, it will work as for previous icon you met, or "for alls" like the level builder icon.


Different levels of knowledge

About the plugin matter we can thinking to four level of knowledge:
  1. First Level (the lowest: final users)
    The simple ability to use plugin created by other people.
    This operation is really very easy, since just only knowing how to install a plugin with NG_Center and then the new skills will be available like it happened with new features from trng.
    Note: to install a plugin just, select the [Pluging] panel of ng_center and then click on [Install New Plugin] button and select the zip/rar file (or the folder) with the plugin's files.

  2. Second Level (basic: customizers)
    For "customizers" I mean level builder that wish change a bit the source of plugins created by others, whose are available the sources, like it happens with plugin_tnrg, for example.
    This second level doesn't require to be able to create a plugin from scratch but however it's important understand a bit how to use Microsoft Visual Express, and reading the source code understanding what you are reading.
    A customizer can easily change sounds, speed, distance of hardcoded new enemies, vehciles, or AI behaviors simply reading the commented sources, changing what he wish, and then build the new customized version of that plugin.

  3. Third Level (advanced: level builders skilled in advanced scripting)
    At third level we find level builders that, having a good experience about advanced scripting (trng variables, conditional triggers, organizer) they have already programming abilities.
    They have only to learn the syntax of new C++ functions and informative structures (GET, FLOOR, FIND ect) to create from scratch their plugin with new enemies, AI skills, veihicles ect.

  4. Fourth Level (programmers)
    This level is higher of "advanced" supposing that these programmers are able to program also in assembly language (other than C++, of course)
    Using assembly language they can get access to low level code of tomb raider and patch it, rather creating only from scratch new features in C++ language.





Installing Microsoft Visual Express 2010


To convert our plugin sources in a dll library that was able to work with trng engine, it's necessary a compiler program.
I suggest to download and install Microsoft Visual Express 2010 because the plugin sources have been developed own with this release, anyway it's probable that they will work fine also with more up to date release and, for sure, with professional visual 2010 release.
However the higher available releases (2012 and 2013, currently) cann't run under windows Xp, while Visual Express 2010 is last and higher version having yet support for WindowsXp that is the best platform to develope our tomb raider adventures.

The Visual Express 2010 is free, just only registering on microsoft website and the program will work forever.

Download and Install now this program: How to download and install Microsoft Visual Express 2010 program .

How to prepare the Level for this Tutorial


Loading the starting plugin project
Loading the starting plugin project in Visual Express 2010 compiler.
You find this project in "PLUGIN_SDK_STORE\Plugin\plugin_trng_start_vc2010_sources" folder.

Note: if you skipped the previous link about How to download and install Microsoft Visual Express 2010 program , you should at least read the How to set correct properties for our Project chapter.


Copy the starting Script files in NG_Center
You find the starting scripts in "PLUGIN_SDK_STORE\Level demo\Scripts\Start_Scripts" folder.
Copy them to "trle\script" folder.


Copy the wad files in TRLE folder
You find the plugins.wad files in "PLUGIN_SDK_STORE\Level demo\Wads" folder.
Copy them to "trle\graphics\wads" folder.


Copy the title.tr4 file to TRLE folder
You find the title.tr4 file in "PLUGIN_SDK_STORE\Level demo\" folder.
Copy it to "trle\data" folder


Copy the project files to TRLE folder
You find the project files (.prj and .tga files) in "PLUGIN_SDK_STORE\Level demo\Project\Start_Prj" folder.
Copy them in a new sub-folder, located in "TRLE\Maps\" with a name like "Plugin Demo"


Copy the image file to TRLE folder
You the (only one) image used by plugin in "PLUGIN_SDK_STORE\Level demo\Project\Pix" folder.
Copy it to "trle\pix" folder.


Build all
Now you should build:






Gaining confidence at our plugin source


If you have already installed Visual Express program and set the configuration for plugin_trng project (previous paragraph) , now we can to gain confidence at plugin sources.





Start Visual Express 2010, and in first page, below "Recent Projects", click on "Pugin_trng" link (see left side of above image) to load newly our project.
When the project has been loaded, click on window or panel named "Solution Explorer", to see all files of plugin_trng project (see right side of above image).

Looking that long list of single files (bass.h, constant_mine.h, DefTomb4Funct.h and others) it seems a very complicated matter but these multiple files are like the different wad files you use to build a .tr4 file.




We'll have the same situation with our plugin project, where we have different files to get at end a final "plugin_trng.dll" file.




Another similarity is that, like about wad files, we'll not use really all these files in direct way. The 90% of our work it will happen always in only one source: the "plugin_trng.cpp" source, the main source.

Meaning of different source files

If you wish a full descripion of all source files, also the secondary files, you can see Table A: Source File Descriptions





Kick-Off

Ok, let's do first test.
To verify that the plugin sources have been correctly installed and set, now we'll build our project and we'll verify if it will be loaded in tomb4/trng engine.
However, before building it, we have to add some code to see something in game when our code will be performed.,,

Learning to move in the Editor of Visual Express 2010


Go to "plugin_trng.cpp" source.
You have different way to select as current source a given file.
You can perform a single click with mouse over the tab having the wished source name.
In our case click on [PlugIn_trng.cpp] panel name.




Another way is to use the Solution Explorer window (or frame).
In this case you have to do a double click on the name of wished source to do of it the current source.
See picture at left.



While the plugin project has different sources (.cpp and .h files), any single source.cpp has different sections, named "functions" or "procedures".
You'll type your code in one of these functions in according with what you wish get.
About the importance of position where you type your code in the Plugin_trng.cpp source, see the Table B: description of Plugin_trng.cpp source
To select the wished function you can scroll the source as within a common text editor (indeed our sources are basically common text files).
Another way is to perform a finding of text, using the "Find in Files" tool:


Clicking on little "Find" icon (see red circle in above picture) you open the "Find in files" window.
You can type the text to find and specify in what scope to look for it.
"Entire Solution" is to look for it in all sources of plugin project.
"Current project" will look it only in current selected source.
"All open documents" it will be looked also in extern documents you opened in the editor
ect.
In spite we'll use very often the "Find in files" feature, when we are looking for function names, the better way is to select its name from the list of functions we have in top-right corner.


Clicking on litle down-arrow at right (see red circle in above picture), you open the list of functions of current source.
Since we have already selected as current source the "PlugIn_trng.cpp", we see all functions of this source.
Now we can scroll the list to reach the name "cbCycleBegin" and perform a click on it.


Well, now we can type our code in cbCycleBegin() function.


cbCycleBegin() function

The cbCycleBegin() function will be called everytime the engine is building a frame of game.
The "cb" at begin are for "call back", because this is a function that will be called from trng engine when something is happening.
In our case the happening is the performing on game cycle, so this function will be performed 30 times for second when the program is in game mode.
It will be no called when the program is in Inventory (or other paused screens) mode, or when the program is loading a level ect.


What is a callback?
If you wish understand better the meaning of "callback" see the Callback and TRNG Services help file.







Introduction to Plugin Exercises

In following chapters of this tutorial we'll learn better how to type code and to know the commands to use and their syntax.
We'll do different lessons using the form of exercises: we'll learn some commands and their syntax, little by little, while we meet them. Anyway I'll give also links to have a better description of different topics.
I suggest to try really to type all these exercises, it's not important if many of these execercises will have a weird or boring target because the true and final target it will be only to learn how to use plugin tools.
For this reason it's important you typed (wrote) really the code showed in these execercises, since that, limiting yourserlf, to read them, nodding, saying "oh yeah I understand", it should be only a good way to learn ... nothing.
You have to move your little gold hands and take confidence of Visual Express editor and the syntax of all plugin commands typing all following code


Summary of Exercises

Exercise Contents
Exercise 1: The Magic Flare [Magic Flare] Give to Lara the chance to fly when she holds the flare
[Dangerous Walls] Lara will be injured when she touches red walls
[Ceiling TeethSpikes] Activate teeth-spikes placed on the ceiling when lara will be very closed to the ceiling

Introduction to Get() and Find() functions.
How to build the project.
Description of auto enumerate facility: enumGET, enumFIND ect.
Introduction to C++ syntax: brackets, functions, for statement, vectors, conditional operators, structures.
How to manage input game commands, working with collision box of moveables.
Introducion of MyData structure, local and global variables.
Coordinate system in tomb raider: room structure.
Introduction to usage of trng trigger called from plugin code: the PerformActionTrigger() function
Exercise 2: Lost in Space [Robotic Cube] code to move a cube like a robot, avoiding walls,ceiling or bad slopes

How to create new triggers.
Syntax of TRG file.
Hot to catch the activation of our triggers in game.
The Progressive Actions
The PerformMyProgrAction() function
The CheckFloor() function and its FLOOR structure
Slope types.
The IsFreeWay() function
The UpdateItemRoom() function
The GetIncrements() function
The GetItemSize() function
Exercise 3: The Cleaner Robot [Cleaner Robot]

How to create a new standard moveable object.
The Slot structure.
The management procedures of moveable objects.
How to detect the collisions between robot and Lara or other moveables
How to add special effects to some mesh of moveables
Usage of some tomb4 functions:
bool TriggerActive(StrItemTr4 *pItem);
bool TestBoundCollide(StrItemTr4 *pItem, StrItemTr4 *pLara, int LaraRadius);
bool TestCollision(StrItemTr4 *pItem, StrItemTr4 *pLara);
void ItemPushLara(StrItemTr4 *pItem, StrItemTr4, *pLaraStrCollisionLara *pCollLara, bool TestChangeLaraAnim, int Parameter);
void AnimateItem(StrItemTr4 *pItem);
void TriggerFlareSparks(int CordX, int CordY, int CordZ, int Red, int Green, int Blue, DWORD TestSmoke, DWORD Unused);
void GetJointAbsPosition(StrItemTr4 *pItem, StrMovePosition *pCoordinate, int JointIndex);


Exercise 4: The Star Wars Robot [Star Wars Robot]

How to discover the rasing/declivity respect to the item: the DiscoverySectorType() funcion
How to detect the presence of Lara: the CheckDirection() function
How to detect the presence of static items: the DetectCollisionWithStatics() function
How to detect the presence of Box (gray) sectors.
How to create our own Customize command
How to discover the height of items in game units
How to use the Debugger of Microsoft Visual Express
How to handle triggers requiring Param commands in the script
How to create new script commands
Input arguments of SoundEffect() function
Exercise 5: The Swinging Crane [Swinging Crane] In automatic mode

The AssignSlot command to support new Objects
How to read from code the data of AssignSlot commands
The default draw procedure
How to move an object (The Crane) to get it near to Lara
How to get the chance to customize the sounds for our new objects
How to read data from Customize=CUST_SFX_DEMO command: the GetSFX() function
How to create a proportional Shadow: the Crane_ShowShadow() function
Exercise 6: Driving the Crane [Drivable Crane] In manual mode

- The default procedures used by Animating objects
- The CheckPositionAlignment() function
- How to enable a vehicle
- How to animate lara during a vechicle phase: the AnimateItem() function
- How to set a new global trigger event: the DetectedGlobalTriggerEvent() function
- The flowcharts used to describe a procedure
- How detect collisions between items: The IsCollidingWithSomeItem() function
- How to forbid that an item enters in other item: The ItemPushAwayItem() function
- How to initialise a new progressive action
- Adding splash and ripples on the water surface: the SetupRipple(), TriggerSmallSplash() and Splash() functions
- How to do align lara to ideal position for switching: The AlignLaraAtPosition() function
- How to handle a numeric value in ocb field: the bit mask
- How to move pushable items with ng features: The StartMoveItem() and EndMoveItem() functions
- How to add ocb descriptions in OCB list of NG_Center: the ".ocb" plugin file
- How to enable triggers of some sector: The TestTriggers() function
Exercise 7: The Mech Warrior [MechWarrior]

- How to handle a real "moving" vehicle
- How to handle get-in and get-out actions
- How to fix the collision box of vehicle to manage get-in action
- How to have a customizable Camera View for vehicles: the SetCamera() function
- How to get a Set Position anim command from code
- How to handle an object/vechicle only with State-id changes
- How to handle collisions for large objects/vehicles
- How to align vechicle in according with wall collisions
- How to create an advanced collision function using mnemonic constants to remember different collision types
- How to manage continue input commands
- How to avoid skipping of collision control in whiletime of swap animations
- How to filter input commands in according with current state-id
- Homeworks: suggestion about how to complete Mech-Warrior vehicle






Exercise 1: the Magic Flare

This exercise begins in room 0 of plugins project




Click inside of brackets of cbCycleBegin() function, to have the caret and type this text:


void cbCycleBegin(void)
{
          Get(enumGET.
}

Look fine and you see the "." (period) at end of enumGET
Now you should see this:


A list with different mnemonic constant will be showed.
Scroll the list and select the item LARA.
Note: once the wished name has been highlighted you can use space bar, comma [,] or closed round parenthesis to confirm it.
Now type a comma...


You see a tool tip that remember you the arguments required by Get() function.
It's a nice thing.
Now complete the row in this way:

          Get(enumGET.LARA, 0,0);


Get() is one of many functions of trng.cpp source.
These functions are like commands that you can use to reach your target.
See Function Collection to see the full list.



Commands and Functions

What is this Get() function and what is its target?
While in script.txt the commands have a syntax like this:

CommandName=Argument1, Argument2, Argument3

Where an equal sign (=) divided command name by its arguments (said also parameters or fields)
In C++ function we have this syntax:

FunctionName(Argument1, Argument2, Argument3);

Where the arguments will be enclosed in a pair of round parenthesis.
Another news is the semicolon character at end ";".


Briefing about C++ syntax

Now we'll do a very short briefing about some syntax rules in C++ language (our project is mostly in C++ language. Anohter part is assembly but that is only for programmers)


If you wish have a complete quick-reference about C++ syntax you can see Basics of C++ Language
Anyway it's better studying it after having completed this first introduction to understand some topics gradually.


Each instruction in C++ has to have at end a semicolon character ";".
This is a way to inform the compiler that the instruction ends in that point.
It's a bit weird, ok, but there is an advantage: since the compiler discover the end of line from this ";" semicolon character, we can type a long instruction between more rows,with no need to specify some break-line character.
While in script.txt, when we have to type very long line, we use this syntax:

PuzzleCombo=           5,1,Ornate Handle,          $0002,$0500, >
                                        $4000,$1000,$0000,$000a


With the "greather than" character to inform that the row continue in following line.
In C++ we can move us in following linee with no special character.
Example:

          if (FromNgleStaticIndexToTomb4Indices(
                    Index, &RoomIndex,
                    &StaticIndex)==false) return false;

And compiler will have nothing of bad to say, because it will parse the text until to reach the ";" character and then it will compact above three lines like they were only one.


Bracket parenthesis

The bracked parenthesis are used very often in C++ language.
They are used to gather together a group of instructions (function calls and matematic operations).
For instance, since a function is own a group of instruction, each function has a syntax like this:

ReturnValue FunctionName(Arg1, Arg2, Arg3)
{
... instructions of this function

}


What is Get() function?

Get() function is main locator of resources.
All stuff about moveables items, rooms, statics, collision, script commands ect, are inside a huge structure (structure = a group of variables or other sub-structures) named "GlobTomb4".
To have a direct access to these data you use this syntax:


Trng.pGlobTomb4->

Now you'll se a lot of variables and sub-structures in a tree hierarchy.


The problem is that this structure is really huge.
So it's not easy for you understand where find that you wish.
The Get() function is a locator because it accepts as argument a mnemonic constant GET_... to choose the main resource, and it extracts them for you and place them in a littler structure named GET.
More, you can use Get() function also like a way to understand better how to access to GlobTomb4 structure, studying its code you find in "trng.cpp" source:


Looking above extract from Get() function body, you can see where Get() function found Lara structure and the moveable structure of item with a give index.
Probably that code is yet a bit complicated for you but after you took confidence at C++ syntax, you'll be able to read it easily and you'll discover how to access yourself to all data in GlobTomb4 structure.
About difference between Get() function and GET structure (where there will be the returned values of Get() function) we discover another difference between syntax in plugin project respect to script commands.
In C++ language all names are case-sensitive. It means that Get is different than GET.
Also variables and structure and function names follow same rule.
So take care to type correctly the names with capitals or lower letters where it needs.

What is enumGET ?

The Get() function requires like first argument a mnemonic constant of GET_... type.
You can find all mnemonic constants in Tomb_NextGeneration.h source:

// constants for Get() function (note: you can use also enumGET autoenumerate
#define GET_LARA 0                    // returns values in GET.pLara-> and GET.LaraIndex INPUT: none
#define GET_ITEM 1                    // returns value in GET.pItem-> (moveable object).INPUT: Index = index of moveable + (NGLE_INDEX if it is a ngle index)
#define GET_STATIC 2                    // returns value in GET.pStatic-> (static object) INPUT: Index = (index of static + NGLE_INDEX) or (Index=Room), SecondaryIndex = static index if it was missing NGLE_INDEX, or unused
#define GET_ROOM 3                    // returns value in GET.pRoom-> (room structure) INPUT: Index = index of room (no NGLE_INDEX, use the second number you see in room list of ngle program)
#define GET_ITEM_COLL_BOX 4                    // returns value in GET.pCollItem-> (collision box of moveable item) INPUT: same of GET_ITEM
#define GET_STATIC_COLL_BOX 5          // returns value in GET.pCollStatic-> (collision box of static item) INPUT: same input of GET_STATIC
#define GET_STATIC_VIEW_BOX 6 // returns value in GET.pViewStatic-> (view box of static item) INPUT: same input of GET_STATIC
#define GET_DOOR_OF_ROOM 7                    // returns value in GET.pDoor-> (structure of "room" door) INPUT: Index = index of room, SecondaryIndex = index of door
#define GET_INFO_LARA 8 // returns values in GET.LaraInfo. (structure with various infos about lara) INPUT: none
#define GET_MY_PARAMETER_COMMAND 9 // returns value in GET.pParam-> INPUT: Index= PARAM_ value, SecondaryIndex = (optiona) id (first field after PARAM_ value) of Parameters command or -1 if you wish omit this input value
#define GET_MY_CUSTOMIZE_COMMAND 10 // returns vlaue in GET.pCust-> INPUT: Index = CUST_ value, SecondayIndex = (optional) value of first field after CUST_ value or -1 if you omit this input value

Reading in this source (see above extract) you find also a short description for each constant.
Theoretically you should use own the single constant: "GET_LARA" as argument, omitting the enumGET, anyway I added this facility for some mnemonict constant groups that you could use very often.
The advantage to use auto-enumerate list, typying "enum" (in lower case) and then (in capital letter) the constant prefix, is that in this way the compiler will show to you the possible chances.
Missing auto enumerate, you should everytime go to read the tomb_nextGeneration.h file, copy that text and then going back to your main source to paste in that position that mnemonic name.
Unfortunately not all mnemonic constants have a "enum" form, so when you type enumSOMEPREFIX and typing the . (dot) character you don't see any list, this means that prefix has no auto enumerate and you'll have to get its name in old manner.


Our first special effect

Now we have to complete our first code.
The first row of code:

          Get(enumGET.LARA,0,0);

It will give the lara structure in GET structure.
We need of another stuff for our first special effect, some infos about lara status.
So we use newly Get() function to have more generic infos about lara:

          Get(enumGET.INFO_LARA,0,0);


To change something in game, and verifying that our plugin is working, we'll do fly lara when something happens, a sort of global condition since we place this code in cbCycleBegin() that will be performed always.
Let's say that this condition is when lara holds a flare in his hand.
So, now we'll check the condition "is lara holding the flare?" and when the condition is true, we'll decrease the Y coordinate of Lara, because in 3d tomb world the top is with lower numbers.

          if (GET.LaraInfo.TestIsHoldingItem == true) {
          }

With above row we check if lara is holding some item in her hands.
All code we'll type inside the pair of brackets, we'll be performed only when the condition of if ( ...) is true.
We have to verify also if the item that lara is holding is own the flare.
So we'll type in { } brackets of first if() another condition:

          if (GET.LaraInfo.TestIsHoldingItem == true) {
                    if (GET.LaraInfo.HoldedItem == enumHOLD.FLARE) {
                              // lara is holding a flare in her hand
                    }
          }

Now we have only to move up in 3d world lara.
Just decreasing her Y coordinate, and we'll use the GET.pLara structure to modify this value:

          GET.pLara->CordY -= 32;

Once added above line we have completed our special effect:


Now we have to test it...


Building the project

Before giving the command to build the project and get the "plugin_YourName.dll", you should verify to have set right values for version to build:


Look the setting in red circle of above image.
It's better you choose the [Debug] version, because it is like when you enable Diagnostic in script command. You'll have some help while you are developping your project.
While, when you'll have completed it and you want give to other your plugin then it will be better select the [Release] version, because it will be more compact and faster.
About [32 bit] setting I discorauge to use [64] bits because our plugin is linked so strongly with the 32 bit tomb raider program that there could be many troubles. Anyway I've never tried to use 64 bits.

Now we verify if we put some mistake, the compiler will inform us about that...


Go to [Debug] menu and select the item [Build Solution].
You have also the keyabord shortcut F7 to set this command.
Now wait a moment that compiler converted your source files in plugin_YourName.dll
If all it's ok, you'll see this final message:


The good news is when you read the message "Build: 1 succeeded, 0 failed..."
Because our plugin solution has only one project so when "1 succeded" all it's working fine.

Copy the plugin .dll in trle folder

Now we have to copy the just built Plugin_YourName.dll to the trle folder to test it in game.
Navigate in your plugin folder open it, and then open also the sub-fodler named "Debug":


We look for plugin.dll in [Debug] because we chose the [Debug] version before building the project, of course.
When you'll use the [Release] version before building, then you'll find the plugin.dll in [Release] sub-folder.
Now in [Debug] folder you copy the "Plugin_YourName.dll" , and copy to "trle" folder you had set in settings of plugin project.

Finally, we have only to launch tomb4.exe program and to verify what it will happen in game.

What will it happen in game?

Now in game move lara in bigger room and try to extract the flare.


Ok our first attempt it's gone bad: lara is not flying (picture A).
Anyway if we try to do a jump up from still while we have the flare in the hand, we see (picture B) that lara goes upper than usual, when we have not the flare (picture C).
Better than nothing but why didn't it work?
Because tomb raider engine perform some automatic adjustments when it believes there is a misplacement of some moveable.
In this case we move upper the floor lara but tomb engine knows that when lara is in stand-up position she should keep her feet on the floor, so the engine reset our change of Y coordinate and set that value to current floor value.
Differently, when we do jumping lara the engine understand that is normal that Y coordinate of lara was different than floor coordinate because she is jumping in the empty. In this other case then engine don't reset our change of Lara Y coordinate.
Ok, it could be fine that to fly upper we have to jump but it remains the problem that lara is not able to reach upper floor. Look upstairs in the room: lara will have to fly over and over until to reach the top room.
So we can try to change our code.
Instead modifying Y coordinate, we could try to change vertical speed of lara.
So we'll change our code in this way:
We'll replace the instruction:

          GET.pLara->CordY -= 32;

with this instruction to change vertical speed:

          GET.pLara->SpeedV = -40;


Now we'll try again.
Build the project (F7) and then copy the plugin_yourname.dll from "debug" folder to "trle" folder and launch tomb4 program.


A bit better but ...

Now lara goes on upstairs, a bit slowly but ok...


But we have yet some problems...
It's not possible drive her, she moves only up but how can we choosing the direction where she will look?
That we call orienting or facing it could be also called "direction". The H orient is the direction where lara is looking.
Now we could adding some code to be able to change the direction where she is looking.
Since the direction (or horizontal orienting/facing) is the field of lara structure named "OrientationH" we have to change that value to do turning lara.
But we have to change it only when we'll receive input command for right or left directions.

Reading Game Command Input

To know what input command has been set in game we have to require to Get() locator also infos about Input.

          Get(enumGET.INPUT, 0,0);

With above code we get input data in GET.Input substructure.
Now, we'll perform this compute:
When player hits RIGHT we'll add a value to "OrientationH" of Lara, while when player chose LEFT we'll subtract same value to "OrientationH".

          if (GET.Input.GameCommandsRead & enumCMD.RIGHT) {
                    // player set RIGHT command
                    GET.pLara->OrientationH += 0x200;
          }
          if (GET.Input.GameCommandsRead & enumCMD.LEFT) {
                    // player set LEFT command
                    GET.pLara->OrientationH -= 0x200;
          }

So, our final code it will be:


Now, try it with same procedure: hit [F7] to build the project and then copy the "Plugin_yourname.dll" from "Debug" folder to "trle" folder.
Launch tomb4, extract flare and try to do turning lara with LEFT or RIGHT commands.



Ok, now we can to do turn lara where we wish.


Structures and testing Flags



Before continuing, came back a moment to last code we added:

          if (GET.Input.GameCommandsRead & enumCMD.RIGHT) {
                    // player set RIGHT command
                    GET.pLara->OrientationH += 0x200;
          }
          if (GET.Input.GameCommandsRead & enumCMD.LEFT) {
                    // player set LEFT command
                    GET.pLara->OrientationH -= 0x200;
          }


About the part: "GET.Input.GameCommandsRead " we have to understand how the structures work.
A structure is a group of variables but other that variables there could be also other structures inside.
GET (typed in capital letters) is a structure, containing these variables and substructures:

          StrItemTr4 *pLara;
          int LaraIndex;
          StrItemTr4 *pItem;
          StrMeshInfo *pStatic;
          StrRoomTr4 *pRoom;
          StrBoxCollisione *pCollItem;
          StrBoxCollisione *pCollStatic;
          StrBoxCollisione *pViewStatic;
          structStrDoorTr4 *pDoor;
          StrLaraInfo LaraInfo;
          StrGenericParameters *pParam;
          StrGenericCustomize *pCust;
          StrInput Input;


(note: You find it in "structures.h" source, with the name "StrGetLocator")

When we wish read (or write into, or compare) the value of single variable of our structure, like the variable "LaraIndex", we'll type the name of main structure "GET" then a dot "." and then the name of variable "LaraIndex":

          GET.LaraIndex

Now this text "GET.LaraIndex" is like a single variable, we have had type the "GET." in front to explain to compiler where find this variable (it was in GET structure) but now it is like a single variable and we can use it like we used a common Alfa variable:

          // assign to Alfa the value 5
          Alfa= 5;
          // compare if Alfa value is greather than 3
          if (Alfa > 3) {
                    // code when Alfa is greater than 3
          }
          // in same way we do with LaraIndex
          if (GET.LaraIndex > 0) {
                    // code performed when LaraIndex is greater than 0
          }


When we wish access to a sub-structure inserted in a main structure we do the same. We continue to type "." dot to follow each path until to reach our final variable.
For instance in our code:

          if (GET.Input.GameCommandsRead & enumCMD.RIGHT) {


There is a variable "GameCommandsRead" that is in Input structure and Input structure is in GET structure, so to reach final variable "GameCommandsRead" we'll have to type:

          GET.Input.GameCommandsRead



Dot or arrow?



Looking another row of our code:

          GET.pLara->SpeedV = -40;

We should wonder: why have we used that little arrow "->" instead by using the usual dot "."?
Because the "." dot will be used when the variable or structure is INSIDE that structure, but in our case the structure of lara is not in GET structure but it is very very far, in the memory of tomb raider program. To reach that far distance we have to use that little arrow "->"
To discover when we have to use the dot "." or the arrow we can see if the name at left begins with a lower case "p" or less.
That "p" is for "pointer", since it points to a far memory zone where there is that structure.
The "pLara" begins with "p", so it is a pointer and we have to use the arrow "->" to reach the variables stored in that far structure.


What is the operator: "&" ?



Looking the code:

          if (GET.Input.GameCommandsRead & enumCMD.RIGHT) {

We find the condition "GameCommandsRead & enumCMD.RIGHT"
We have already discovered what is "enumPREFIX", it is a facility to get a mnemonic constant.
But why have we used the "&" operator instead by using simply a "=" operator?
Looking the real value of CMD_RIGHT we discover it is 8

#define CMD_RIGHT                    0x00000008


If, when in game the player hit RIGHT command, in variable GameCommandsRead it will be set the value 8, why should it not work typing the condition in this way:

          if (GET.Input.GameCommandsRead = 8)

Well, as first point, the condition "is even", in C++ language requires to use two even signs "==", otherwise, using only one "=" it is an assigment as if you wished assign to GameCommandsRead variable the value 8.
Anyway also typing in right form the condition:

          if (GET.Input.GameCommandsRead == 8)

There is a problem...
It's true that in some circustances it could work, but what could it happen when the player other to press RIGHT key it keep down also ALT key (for jump) ?
The value for JUMP command is:

#define CMD_JUMP                    0x00000010          

The value 0x0000010 is hexadecimal and in decimal it will be 16.
When player keep down RIGHT and JUMP in GameCommandsRead variable there will the value 8+16 =24
When program will test the condition:

          if (GET.Input.GameCommandsRead == 8)

It will be false, because 8 (the value we typed in the condition) is different than 24 (the value in GameCommandsRead when player hit RIGHT + JUMP)
For this reason we cann't use "==" but we have to check only for the presence of a single bit (or flag) at once.
When we type "&" this is a binary AND and it means: verify if the bit with value 8 is present in this variable, I'm not interested about other bits, presents or less.
In our example using the condition:

          if (GET.Input.GameCommandsRead & enumCMD.RIGHT) {


It will be true everytime the player hit the RIGHT command, indifferently if other commands have been set or less.


What are "+=" and "-=" operators?

To do turn lara we used this instruction:


                    GET.pLara->OrientationH += 0x200;


We used that instruction to add to "OrientationH" variable (inside of Lara structure) the value 0x200 (hexadecimal, while in decimal it will be 512)
The "+=" is simply a compact syntax to increase a variable by a given number.
We can type that instruction in expanded syntax:

                    GET.pLara->OrientationH = GET.pLara->OrientationH + 0x200;

Result is the same, we set in OrientationH variable the sum of previous value of OrientationH variable added to 0x200 value.
Using += we do the same, typing a shorter text that it will be also a bit faster to be performed.
The operator "-=" works in same way but in this case is to reduce the value of variable by given value:

                    GET.pLara->OrientationH -= 0x200;


What is the reason of that spaces at left of instructions?



Looking last code we typed:

          if (GET.Input.GameCommandsRead & enumCMD.RIGHT) {
                    // player set RIGHT command
                    GET.pLara->OrientationH += 0x200;
          }
          if (GET.Input.GameCommandsRead & enumCMD.LEFT) {
                    // player set LEFT command
                    GET.pLara->OrientationH -= 0x200;
          }

We see that some instructions begin first and other more at right: is there a reason?
Yes there is, of course, anyway it's not necessary but only a good habit.
For instance we can also type same code in following way:

if (GET.Input.GameCommandsRead & enumCMD.RIGHT) {
// player set RIGHT command
GET.pLara->OrientationH += 0x200;
}
if (GET.Input.GameCommandsRead & enumCMD.LEFT) {
// player set LEFT command
GET.pLara->OrientationH -= 0x200;
}

But comparing the two lists, don't you believe that the first was a bit more clear to be understood?
In C++ language there are many statement that accept a pair of bracket parenthesis "{ }" and inside of this pair, there will be the code affected from this parent instruction.
For instance the "if" we saw above introudce a conditon (between round parenthesis) and when that condition it's true, it will be peformed only the instruction in following couple of bracket parenthesis.
In this situation if we use different indents, for different logical blocks of code, it will be more easy recognize the limits of that block.
Above examples were only on a very short code, but in some complicated sources we could have a structure like following:

          if (a > b) {
                    switch (a) {
                              case 1:
                                        if (b == 0) {
                                                  a= b+1;
                                        }
                                        break;
                              case 3:
                                        switch (b) {
                                                  case 0:
                                                            return -1;
                                                  case 2:
                                                            a= a+b;
                                                            break;
                                                  default:
                                                            a=0;
                                                            b=0;
                                                            break;
                                        }
                                        break;
                    }
          }

Ok, it's a bit complicated but looking the different distance from left margin, you recognize the logical block child and its parent instruciton.
But if we had typed same code omitting indention, should be it yet readable?

if (a > b) {
switch (a) {
case 1:
if (b == 0) {
a= b+1;
}
break;
case 3:
switch (b) {
case 0:
return -1;
case 2:
a= a+b;
break;
default:
a=0;
b=0;
break;
}
break;
}
}

Absolutely unreadable.

The rule to choose how many tabulations insert

The generic rule, about how many tabulations introduce in each row, it's to move one more tabulation column at right, everytime you open a bracket "{", and reduce of one tabulation (coming back at left) everytime you closed a bracket parenthesis "}".
Really there are other situation where you should add new tabulation: everytime you are typing the code owned by a previous parent instruction.
For instance in switch() instrucion, you'll add a tabulation also after any "case" since after this istruction there will be the code owned by that case.


How to add to Visual Express some buttons to help you in editing

About indention but also comments and book marks, you can read the help about: How to customize Visual Express
In this little help, it will be described how add some button on top line to indent whole blocks of code, or convert the code in comments or viceversa.
Also buttons to place bookmark and to move between these marks, are very comfortable.



Complete our Magic Flare effect

Now we have to complete our new effect, because there is yet some problem to fix.
Thanks to last changes we are able to do turn lara in any direction but the forward movement is almost absent.
We wish move lara while she is moving, choosing the direction (already done) but also moving her in horizontal direction to reach wished target.
If you look carefully lara while she is floating in the empty you see that she is really moving forward but it happens in very light way.
Since the speed of movement in horizontal way is set in "SpeedH" field of lara structure, we can increase that value to do move her faster.


          GET.pLara->SpeedH = 32;

Why in this case haven't we used the syntax:

          GET.pLara->SpeedH += 32;

?
Because the speed is already a value that it will be added every cycle to coordinates of lara, it will be the tomb engine to add continuosly that value.
If we had typed "+= 32" the speed it will be increased 30 times for second and only after one second of it will be becomed 32*30=960. I.e. engine will add 960 (and more) to lara's coordinates every 1/30th of second. Since one game block is 1024 units, lara should be throwed away from whole level in few seconds.

When we set 32 as speed, differently it means that lara will be moved covering the distance of 32 units for each frame. This means that her speed in game will be about of (32*30=960) of less than one sector for second that is a reasonable value.

Also for our horizontal speed we should change it only when there is some game command, otherwise lara will be always moving also when player is not setting any command.
We can use command for "go forward" i.e. the UP arrow.
So the code we'll add it will be:

          if (GET.Input.GameCommandsRead & enumCMD.UP) {
                    // player set UP direction command
                    GET.pLara->SpeedH = 32;
          }

Now try newly our plugin: F7 key and then copy .dll from "debug" to "trle" folder and launch the game.




Ok, now we can move lara in any wished direction.


Our first function

Our first effect is not so long, anyway it's better if we learn to keep in order our sources because in the future we'll add plenty of codes in cbCycleBegin and gathering different type of code in same function it could be a good way to mess our sources.
A good way to put in order is to divide all our code in different function, each function with his meaningful name that will remember to us what is that code.
We can begin own with our first effect, moving our code for flare in a new function we could call "MagicFlare".
When you create a new function and you already know from where it will be called, it's better place this new function immediatly upper (in the source) respect from where it will be called.
In this way the compiler will know its name when we call it.
So now click on white space outside and over of cbCycleBegin() function, and type the name and body of our new function:


Now we have only to do a copy & paste.
Select all code we created for the flare, select "Cut" (right mouse click to have pop-up menu) , now click inside of our new MagicFlare() function, and choose "Paste".


Our new function MagicFlare() is ready with all necessary code to perform its "Magic Flare" effect but it is yet isolated if we don't put a "call" to perform its code.
So in the cbCycleBegin() now we'll type the call to perform its code.
It will be simply:

          MagicFlare();





Moving down Lara

There is yet some improvement to do.
Lara now is able to fly upper and to we can move her in some direction but to move down her we have to throw away the flare and lara will fall down quicly.
We should find a less traumatic way to do her landing smoothly.
Since the moving up is given by this code:

          GET.pLara->SpeedV = -40;

If we reduced that value (-40) in absolute way (like -20 or -10) probably lara will begin to move down because there is also gravity simulation by tomb engine that tries to move down her.


Our first global Variable

To change that value we have to replace in that instruction the constant value "-40" with a variable where we'll be able to change that value.
We can call this variable "FlareVSpeed" and its declaration will be

          int FlareVSpeed;

When we use a Variable in our computation we can declare it in the function where we'll use it, but in this case we cann't use this solution.
The variables we declare inside a function will be seen only from that function, and this should be ok for us, but another limitation is that, that variable, will keep its value only in single run time of that function but when our function will be called newly the old value of our variable will be lost.
The variables declared inside a function are called "local" variables.
To keep the value in a variable (and to see it from any other function) we need to declare a "global" variable.
All variables that we declare outside of all functions will be "global" variables.
So we could declare FlareVSpeed in this position:


It should work but it is also another good way to mess our sources.
Usually you declare the global variable at top of the source and not mixed between different functions.
Another problem is that when we'll have dozen of global variables we could have problems to remember their precise name and target.


MyData global structure

A way to solve these problems is to declare our global variables in a single global structure.
In your plugin there is one single global structure for this target, its name is "MyData"
The advantage to have global variables inside of single (only one) global strcture is that just you remember the name of that global structure and when you'll type in the code the text:

          MyData.

The compiler will show to you all variables present in MyData. In this way it will be more easy remember its right name and typing its exact name in the code.



To see also our FlareVSpeed variable in MyData we have to declare it, inside of declaration of MyData structure, inside of pair of brackets.
Go to source "structures_mine.h" and look for StrMyData declaration:


Now create a new line inside of brackets { } and type our declaration.
We can verify that it worked.
Go back to plugin_trng.cpp source, inside of MagicFlare() function (but really any other function should be the same) and typing newly:

          MyData.

Now we should see also your new global variable stored in MyData, the FlareVSpeed variable.




FlareVSpeed or VSpeedFlare ?

You could wonder because I chose as name "FlareVSpeed" when in english it should be better (!) place first the adjective and then the name.
Anyway using C++ language sometimes it's better place first the most important word (the name) and only after this, the adjectives.
The reason is own about that help that compiler will give to us when we type MyData.
That list of variables will be sorted alphabetically and this is good to locate quickly that we are searching, but if we used common sorting "adjective-name" it will be a bit frustating.
Example:
If we have these possible variables:


int VSpeedFlare;
int XCoordTorch;
int YCoordTorch;
int ZCoordTorch;
int NumberOfFlares;
int BurningTorch;


When we'll type "MyData." the compiler will sort the names of variable and we'll see them in this way:

BurningTorch;
NumberOfFlares;
VSpeedFlare;
XCoordTorch;
YCoordTorch;
ZCoordTorch;


Using a so reduced number of variable perhaps it's not so evident, but this sorting doesn't help us.
When we are looking for "flare" stuff, we wish to type "f" letter to be moved in the list where there are variables about flare.
Or when we are looking for torch, it should be nice if we can move to "t" letter and to see all variables about torch.
With above sorting we don't get this result.
But if we place first main important stuff, the name, and only after it the adjective, the sorting will be very useful to locate quickly what we are looking for.

Using these names:

int FlareVSpeed;
int TorchXCoord;
int TorchYCoord;
int TorchZCoord;
int FlaresNumber;
int TorchBurning;


In the sorted list showed by the compiler we'll have a ideal sorting to locate quickly any stuff group:


FlaresNumber;
FlareVSpeed;
TorchBurning;
TorchXCoord;







TorchYCoord;
TorchZCoord;


In this way looking for "f" we'll see all "flare" stuff, looking by "t" we'll find all "torch" stuff.
More, remember that when you have a list showed by compiler, you can move immediatly to a given letter typing that letter on the keyboard.
If you type "t" the list will be moved to show you the first variable beginning with "t" letter.

It's more comfortable in this way, trust me...


Using a global variable

Now we'll use our FlareVSpeed variable...
We have to change the instruction:

          GET.pLara->SpeedV = -40;

in this way:

          GET.pLara->SpeedV = MyData.FlareVSpeed;


Now we add some instruction to do reduce the absolute value of FlareVSpeed when player chooses some command.
We could use the DOWN arrow, command for this target.

          if (GET.Input.GameCommandsRead & enumCMD.DOWN) {
                    MyData.FlareVSpeed++;
          }

It should fine having also a commad to move up lara, after we had decreased with DOWN her vertical movement.
We cann't using UP arrow, because that will be used to move forward lara in some direction, but we could use ACTION to give to her more sprint in upward movement.
In this case we'll have to decrease the value of MyData.FlareVSpeed, since moving up in 3d world it means having lower numbers.

          if (GET.Input.GameCommandsRead & enumCMD.ACTION) {
                    MyData.FlareVSpeed--;
          }



The "++" or "--" operators

When we wish increase by 1 (or decreasing by 1) a variable we can using "++" insteady by common syntax:

          MyData.FlareVSpeed += 1;
or
          MyData.FlareVSpeed = MyData.FlareVspeed + 1;


note: "MyData.FlareVSpeed++" syntax got the same target of above instructions but ++ is more compact to type and to be performed.

Since we wish that value become lower in absolute we have to increase by +1 the FlareVSpeed, so it will become -39, -38 ect.
We have an important question to solve: we have to initialise the FlareVSpeed with beginning value "-40"
But where could we place this instruction?
Because we cann't place it where it could be performed everytime, otherwise we'll be not able to reduce really its value if every cycle we set newly the variable to -40.
The best solution is to initialise its value only while lara is NOT flying, so we can use as condition the state id = 2, since it means that lara is with her feet on the floor.
We can use this code to initialise the value only when there is stateid=2:

          if (GET.pLara->StateIdCurrent == 2) {
                    MyData.FlareVSpeed= -40;
          }



Our final Code

After all last changes our MagicFlare() function will have this layout:



void MagicFlare(void)
{
          Get(enumGET.LARA,0,0);
          Get(enumGET.INFO_LARA,0,0);

          if (GET.pLara->StateIdCurrent == 2) {
                    MyData.FlareVSpeed= -40;
          }
          if (GET.LaraInfo.TestIsHoldingItem == true) {
                    if (GET.LaraInfo.HoldedItem == enumHOLD.FLARE) {
                              // lara is holding a flare in her hand
                              GET.pLara->SpeedV = MyData.FlareVSpeed;

                              Get(enumGET.INPUT, 0,0);
                              if (GET.Input.GameCommandsRead & enumCMD.RIGHT) {
                                        // player set RIGHT command
                                        GET.pLara->OrientationH += 0x200;
                              }

                              if (GET.Input.GameCommandsRead & enumCMD.LEFT) {
                                        // player set LEFT command
                                        GET.pLara->OrientationH -= 0x200;
                              }

                              if (GET.Input.GameCommandsRead & enumCMD.UP) {
                                        // player set UP direction command
                                        GET.pLara->SpeedH = 32;
                              }

                              if (GET.Input.GameCommandsRead & enumCMD.DOWN) {
                                        MyData.FlareVSpeed++;
                              }                    
                              if (GET.Input.GameCommandsRead & enumCMD.ACTION) {
                                        MyData.FlareVSpeed--;
                              }
                    }
          }          
}


Now we have to try it.
F7 command to build the project and then copy the .dll to trle folder.


Well Done

It works enough fine.
Now we can to do flying lara, choose her direction and also to do plane her smoothly with DOWN command or moving upward newly with ACTION command.


How to handle hardcoded traples


In the Magic Flare level there are some special traps that we have to "animate" with our code.
At left you can see the layout of this level.
There are different rooms casted one over the other.
The target of Lara is to reach the last (at top) room.
While in first room (at first floor) there will be only fixed teeth spikes (on the ceiling), in other rooms there are teeth spikes not yet enabled, always on the ceiling.
If you see the project, inside NGLE program, you'll see that there is no trigger for these teeth spikes items, with only exception for the teeth spikes on the floor of last room.
How will be other teeth spikes enabled?




In above image you can see three rooms of the level. About the missing rooms of 3th and 4th floor they are alike the room of 2nd floor: floating red walls and teethspikes on the ceiling yet to be enabled.
But what should it means those floating red textures on the walls? Danger, of course.
Perhaps in tomb raider 1 there was dangerous walls, while in level editor we can only simulate this damage with some skills.
Now we see own a way to get walls were able to injury lara.


Dangeorus Walls

Our level has three rooms with red (dangerous) walls, i.e. the rooms with indices: 2, 3 and 4
Now we'll do an hardcoded trap to injury lara when she is too closed to these walls.
This feature will work irrespective of presence of magic flare or less, so we'll place this code in another new function that we could call "DangerousWalls"
So, now upper and outside of cbCycleBegin() we'll create our new function:


Our function will work in this way: we'll call this function only when lara is in a room with dangerous walls, then the function will check if lara is very closed to the walls of current (her) room and when she is, she will be injured.

To compare lara position with bounds of room we need of room strcture data.
So we'll type this code:

void DangerousWalls(void)
{
          Get(enumGET.LARA,0,0);
          Get(enumGET.ROOM, GET.pLara->Room,0);

}

We got the Lara structure, and then we required also a room structure, supplying as Index own the room of lara ("GET.pLara->Room")
Now in GET.pRoom we have all data about room where there is Lara.
To discover the lower distance between Lara and the walls of the room just only doing some comparisons.
We'll discover the distance between X coordinate of Lara and X coordinates of two bound limits on X axis of the room, then we'll do the same on Z axis, at end we'll keep only the lower distance computed in absolute way (ignoring the sign, positive or negative)
The value will get will be the least distance between lara and one of four walls.
To keep intermediate results we'll use some local variables, declared inside of DangerousWalls() function:

          int WestDistance;
          int EastDistance;
          int SouthDistance;
          int NorthDistance;
          int EastBoundary;
          int SouthBoundary;
          int LowerDistance;


The cardinal names (east, west, south, north) are about the view from top of our room, where north is upper side in ngle program.

In spite for our computation is not so useful, we can remember the orienting of z/x coordinate in tomb raider world, respect to the view of NGLE program


Above image could be useful only if you wish perform a selective dangerous walls feature, where only some walls of the room are dangerous while ohters aren't.
More interesting is the 2d view of same room, to see its layout in sectors:


In above image there is also the origin set in room structure. It is outside of the room, at top left in above image (the red point)
There is a fake wall (gray) is rounding whole room.
TheX/Z sector size (see belove) in room structure encloses these fake walls. For instance above room will have 6x6 sector size, in spite the floor is only 4x4 sectors.

Keeping in mind above layout now we try to compute the coordinates about internal boundary of this room.

First comparison:
We'll check the absolute distance between Lara and the wall of Z origin (the wall at left (west) in above image)

          WestDistance= AbsDiff(GET.pLara->CordZ , GET.pRoom->OriginZ + 1024);

We used function AbsDiff() to compute the difference between X coordinates of Lara and west wall, to have the distance.
AbsDiff() return the difference between two given arguments forcing always the + sign.
About the "GET.pRoom->OriginZ + 1024" argument, we had to move of one sector the coordinate to skip the fake wall at left (west).

Now we'll have to verify the distance by Lara respect to the wall at Z higher bound (at right (east) in above picture)
In this case there is a little complication, because in room structure it's missing that coordinate.
Fortunately we can compute it easily.
We have not the max Z boundary but we have the number of sector of size of the room, on Z axis (and X axis), so, since we know that one sector = 1024 units, we can find easily the Z higher boundary.
However, in this computation, we have to remember that sector Size includes also fake walls, so we have to reduce the number of sector by 1, to remove the fake walls (always that at west in above image)

Finally our formula will be: OriginZ + (Z_SizeSectors-1) * 1024

Now we add the code to know the Z coordinate of eastern wall (at right in above image).

          EastBoundary = GET.pRoom->OriginZ + (GET.pRoom->Z_SizeSectors -1) * 1024;

Now in EastBoundary variable there will be the Z coordinate of the wall with higher Z coordinates (the wall at right/east in above image)
We'll check the distance also between lara and this right wall, and then we'll keep only that is lower, since we are interested only to discover what is that lowest distance between lara and any wall.

          EastDistance = AbsDiff(GET.pLara->CordZ, EastBoundary);


Now we compare two distance already computed (east and west) and store the lower value in LowerDistance variable.

          if (EastDistance <= WestDistance) {
                    LowerDistance=EastDistance;
          }else {
                    LowerDistance=WestDistance;
          }

In above code we see the "if" linked with "else"
In the brackets of "if" will be perfomed the code when the condition is true, while in the brackets after "else" will be performed the code when the condition is false.
At end we will have in LowerDistance variable the less distance between east and west distance.
Now we have to do same computation on X axis, that corresponding, looking top view of above image, to north and south walls.
Since the logic is the same with only the difference to use X coordinate instead Z, I'll show the remaining code avoding many descriptions:

          NorthDistance = AbsDiff(GET.pLara->CordX, GET.pRoom->OriginX+1024);

          SouthBoundary = GET.pRoom->OriginX + (GET.pRoom->X_SizeSectors -1) * 1024;

          SouthDistance = AbsDiff(GET.pLara->CordX, SouthBoundary);

Now we have also distance with north and south walls.
It reamins only discover what is the lowest distance:

          if (NorthDistance < LowerDistance) {
                    LowerDistance = NorthDistance;
          }

          if (SouthDistance < LowerDistance) {
                    LowerDistance = SouthDistance;
          }

With above code we used some conditions (if) to verify if north or south distance were lower than previous lowest distance (in LowerDistance variable) we had found.
Now in LowerDistance variable there is lowest distance between lara and any wall of current room.
So we can decide what will be the distance from where lara will be injured.
We could try using 128 units.

          if (LowerDistance <= 128) {
                    // injury lara
                    GET.pLara->Health -= 10;
          }

With above code we compared the lowest distance between lara and some wall (LowerDistance) with the limit we set (128). When lara is so closed to some wall to be less or even 128 units, she will lose 10 hp units. Since this will happen for any frame, she will lose (10*30), 300 hp for second, and since she has 999 hp as max value, in about three seconds she will die.


Final Dangerous Walls code


void DangerousWalls(void)
{

          int WestDistance;
          int EastDistance;
          int SouthDistance;
          int NorthDistance;
          int EastBoundary;
          int SouthBoundary;
          int LowerDistance;

          Get(enumGET.LARA,0,0);
          Get(enumGET.ROOM, GET.pLara->Room,0);

          WestDistance= AbsDiff(GET.pLara->CordZ , GET.pRoom->OriginZ+1024);

          EastBoundary = GET.pRoom->OriginZ + (GET.pRoom->Z_SizeSectors -1) * 1024;

          EastDistance = AbsDiff(GET.pLara->CordZ, EastBoundary);

          if (EastDistance <= WestDistance) {
                    LowerDistance=EastDistance;
          }else {
                    LowerDistance=WestDistance;
          }

          NorthDistance = AbsDiff(GET.pLara->CordX, GET.pRoom->OriginX+1024);

          SouthBoundary = GET.pRoom->OriginX + (GET.pRoom->X_SizeSectors -1) * 1024;

          SouthDistance = AbsDiff(GET.pLara->CordX, SouthBoundary);

          if (NorthDistance < LowerDistance) {
                    LowerDistance = NorthDistance;
          }

          if (SouthDistance < LowerDistance) {
                    LowerDistance = SouthDistance;
          }

          if (LowerDistance <= 128) {
                    // injury lara
                    GET.pLara->Health -= 10;
          }
}


We have only to try it...

Testing Dangerous Walls

Our DagerousWalls() function is complete but it is yet unplugged if no other function will call it.
So we have to add a call for it from cbCycleBegin() function.
So go to cbCycleBegin() function, and type inside of it the call for dangerous walls:

          DangerousWalls();

But it should be an error typing only that call, because in this way ALL rooms will be seen with dangerous walls (since our function will be called always), while we have those red walls only in room with indices = 2,3 and 4
So we'll perform the DangerousWall() function only when lara is in room 2,3 or 4:




Please note that I used a local variable "Room" to test the room number and verify if it is one of wished rooms.
The only reason it's because I get a more compact (and a bit faster) code rather to write:

          if (GET.pLara->Room == 2 || GET.pLara->Room == 3 || GET.pLara->Room == 4)


When you use very often a value that is stored in a long variable path (structure and substructure ect) it's better copy to a single variable and then using only that.


Conditional Operators

In above code we see the OR operator.
The double sign "||" is for OR, while "&&" is for AND.
Almost all conditional operators have two characters:
"&&" and
"||" or
! not ( it inverts the result of the conditon)
"==" is even
"!=" is different
">=" greater or even
"<=" less or even
"&" (a single "&" means: test the single bit value, if it is present then the condition is true)


Coming up to the second floor

Since at first floor there are no red walls, we have to move up to the second floor to test our DangerousWalls() function.
It's better landing on the floor because it's more easy testing the walls, walking rather flying in that weird way...
We have to try with all four walls, moving slowing to see if the 128 units of distance works fine.




In seems that's work fine.


The Teeth-Spikes on the ceiling

Now we have to type some code for another trap: the teeth-spikes on the ceiling.
Excluding room of first floor (there, they are already enabled) all other rooms have teeth-spikes to enable and they will move down when they will be enabled.
We wish that lara will enable these ceiling teeth-spikes when she is coming too much closed to that ceiling.
When this it will happen, we'll enable only the teeth-spikes of that room, of course.
Probably we could get this target also using traditional trng stuff, like vertical triggers, itemgroups (they are a lot...) ect.
But using plugin code we have a lot of other shortcuts to reach our targets and in this case it will be also a good opportunity to learn many other plugin stuff very useful for the future...


Ceiling Teeth-Spikes

Since also this feature could be interesting independently of magic flare or red walls, we'll create another new function.
We can name it "CeilingTeethSpikes"
As usual we'll type it upper and outside of function from where it will be called, so we'll place it over cbCycleBegin() function.


We call the CeilingTeethSpikes() function when lara is in a room where there are these ceiling teeth-spikes, like we did with DangerousWalls() function.
So we have to check immediatly if lara is very closed to the ceiling of current room.
We get the lara structure:

          Get(enumGET.LARA, 0,0);

Note: if in caller function (the cbCycleBegin() function, in our case) the Get(enumGET.Lara,0,0); it had already performed, we could simply using immediatly GET.pLara because that value will be yet available.
Anyway in these execercises I'll try to give all for each fucntion that was able to work in stand-alone way, so also if it was called from another function where the Get(enumGET.Lara,0,0); call was missing.

Now we get the room structure where lara is...

          Get(enumGET.ROOM, GET.pLara->Room,0);

And at end we'll compute the distance between lara and the ceiling.
In this case it's better declaring a local variable to host this value:

          int CeilingDistance;

Since about ceiling and floor there are NO fake walls, the compute of distance is very easy:

          CeilingDistance=AbsDiffY(GET.pLara->CordY, GET.pRoom->OrigYTop);

Note: We used AbsDiffY() instead by using AbsDiff(), because the Y coordinates are signed (positive or negative values) while X and Z coordinates are always positive. The only difference between these two functions is about the kind of arguments: AbsDiff() accept unsigned values while AbsDiffY() accepts signed values.

The "OrigYTop" field of room structure is own the Y coordinate of ceiling, while the "OrigYBottom" is the floor Y coordinate.
Anyway looking better above code I see that there is an error.
Are you able to understand what is that?

We are comparing the Y origin of lara with Y coordinate of ceiling but the Y origin of lara is on her feet, while we should compare the top Y coordinate of lara, that closest to the ceiling.


To complicate furtherly the speech, there is the problem that it's neither sure that the Y origin of lara was on her feet. It happens very often but in some state-id, it could be in a different position.
To solve this problem we have to compute the collision box of lara and then to use those coordinate for our comparison.


Discover the Collision Box of Moveable Items


We have already seen the collision box using animation editor of wad merger program.
In the picture at left, it is that white box around lara's body.
The collision box of moveable could change for each frame, since it changes also the disposition of meshes.
It seems complicated compute that box working on relative position of each mesh for each animation but fortunately there is already a function to get the box collision.
We can get the collision box of Lara (or any other moveable) using the "enumGET.ITEM_COLL_BOX" parameter of our Get() function.

          Get(enumGET.ITEM_COLL_BOX, GET.LaraIndex, 0);

The GET_ITEM_COLL_BOX parameter, requires the index of movable whom compute the collision box.
When we call "Get(enumGET.LARA, 0,0);" we got not only the lara structure in GET.pLara but also the index of Lara in GET.LaraIndex variable.
So we required the collision box of moveable with index = GET.LaraIndex, i.e. of Lara.
The returned value will be stored in "GET.pCollItem" that is a pointer (that little "p" in front...) to the relative collision box.
It is "relative" because its values are computed respect to lara when she is in 0,0,0 position.
Therefor to have the absolute values of coordinates we have to add to collsion box values the absolute value of current Lara's coordinates.
Now we declare another local variable to store the Top Y coordinate of lara (where she has her little hands):

          int LaraTopY;

Now we have all necessary to compute the position of top y coordinate of Lara, that closet to the ceiling:

          LaraTopY = GET.pLara->CordY + GET.pCollItem->MinY;

Why have we used the field "MinY" and not "MaxY"?

Because higher position in 3d world are given by littler numbers. An item with a -1024 Y coordinate is one sector upper than another with 0 as Y coordinate.
So the MinY field is the top side of collision box, like the MaxY is the bottom.

Note: the member of structures are variables but they are also called "fields"

Now in LaraTopY variable we'll have the absolute Y coordinate of top side of Lara's body.
We have only to fix that mistake.
We'll replace this line:

          CeilingDistance=AbsDiffY(GET.pLara->CordY, GET.pRoom->OrigYTop);

With this:

          CeilingDistance=AbsDiffY(LaraTopY, GET.pRoom->OrigYTop);

Now we are usign the right Y coordinate of Lara, that in upper side.
Now we have to set what value should have the CeilingDistance variable to enable the teethspikes.
We could try with 64 units.

          if (CeilingDistance <= 64) {
                    // enable all teethspikes of current room, placed on the ceiling
          }


Finding Items - The Find() function

To enable all teethspikes stored in current room we have to find them.
The Find() function has been made for this target.

bool Find(int FindType, short SlotType, short RoomIndex, short Ocb, int Extra, void *pPointer);

The difference between Get() function and Find() function is that while with Get() we'll get items whom we have a sure reference, like an index, with Find() function we'll try to discover if they exist, but we are not sure about result of our research.
We could find no item, one item, or more than one.
First paraemeter of Find() function is a mnemonic constant of FIND_ type.
Anyway also for this mnemonic constant we can use the autoenumerate facility: enumFIND
After having set the kind of FIND_, we'll have to supply some arguments:

SlotType argument
-------------------------
We can say to Find() function to find only moveable with given SLOT (or enumSLOT) value, but also statics have their slot type (enumSSLOT and that first "S" is own for "Static" SLOT)
You can ignore this kind of research typing -1 in this argument

RoomIndex argument
-----------------------------
If you wish reduce the reaserch only in the ambit of a single room you can set the index of room where looking for that item
You can ignore this kind of research typing -1 in this argument

Ocb argument
-------------------
You can also supply a given OCB value, and only items with that given OCB value will be found.
You can ignore this kind of research typing -1 in this argument

Extra argument
-----------------------
Extra is a customizable argument, it will be different for any kind of FIND research
You can ignore this kind of research typing -1 in this argument

pPointer argument
-------------------------
pPointer accept a pointer to some structure.
The "void *" type means "the address of any structure"
Some FIND_ type could require a pointer in this field, for instance the FIND_ITEMS_NEARBY or FIND_ITEMS_SECTOR will require a pointer to X, Y and Z coordinates.
In this case when you have a triple coordinate in some structure, like the structure of an item, you can those values in this way:

          Find(enumFIND.FIND_ITEMS_SECTOR, 0, 0, 0, 0, &GET.pItem->CordX);

Where the "&GET.pItem->CordX" is a way to pass the address of CordX variable of pItem structure. Since after the CordX there are also CordY and CordZ, we gave to Find() function the pointer for the triple coordinates (x,y,z) that it required.

Note: when your FIND research doesn't require a pointer you have to type in pPointer field the value "NULL", that it is like the "0" but for address pointers,



Returned values from Find() function

The returned values will be always indices, and you'll be able to locate the structure linked to those indices using the well-known Get() function.


Working with Vectors

To find all Teeth-Spikes present in Lara's room, we can use this code:

          Find(enumFIND.ITEM, enumSLOT.TEETH_SPIKES, GET.pLara->Room, 0, -1, NULL);

We supplied following values:
Find kind: ITEM ("item" is a synonym for "moveable item", since when we mean "static items" we'll say "statics" or "static items")
Slot: TEETH_SPIKES
Room: GET.pLara->Room (the index of Lara's room, the room where lara is)
Ocb: we set "0" because this is the ocb value for teeth-spikes pointing to south (lowstairs), while the teeth-spikes to ignore, those on the floor, had ocb = 12
while we omitted Extra argument.
Now the indices of all teethspikes in that room, will be placed in these fields of FIND structure:

          int TotItems;
          WORD VetItems[1024];

In TotItems there will be the amount of found items, while in VetItems[] there will be a list of found indices.
If the found item was only one, it will be easy to work with it, we could use this code:

          Index = FIND.VetItems[0];
          Get(enumFIND.ITEM, Index,0);

And now we'll have in GET.pItem the structure of our teethspikes.
But when there are many items how could we do?
In this situation we have to understand better what is a vector (said also "array")
If a variable is like a box with a name printed over, like "Alfa" and inside of the box there is a value, a number.
A vector is like a chest of drawers, where each drawer has a name that is an index [0], [1],[2] and the whole chest drawers has a name, like "VetItems"
For instance when Find() function will find 4 items and these item indices were for example 81, 103, 143, 204
The values in FIND structures will be the following:

FIND.TotItems = 4

FIND.VetItems[0] = 81
FIND.VetItems[1] = 103
FIND.VetItems[2] = 143
FIND.VetItems[3]= 204

Where the [n] is the number of drawer.
When we wish read the content of one specific drawer, for instance that numbered [2], we use this syntax:

Index = FIND.VetItems[2];

and now in Index variable there will the value 143.


How to parse Vectors: the "for" statement

The better way to manage contents of vectors is to use a variable to access to each "drawer", in this way:

Index = FIND.VetItems[i];

Where that "i" inside [ ] is a variable, that can have different values.
If we was able to change the value of i, from 0 (to point to first drawer) upto the last drawer, we could use a single same code to work with all drawers.

The "for" statement has own this target.


          for (i=0 ; i < n ; i++) {
                    // the code typed here will be performed
                    // n times, and i variable will have all values
                    // between 0 upto n-1

          }

For has in round parenthesis three instruction, divided by semicolon characters ";".
First instruction (see above example) "i=0", assign the start value of i variable.
The second instruction "i < n" is a condition and as long as it is true the code (in following pair of brackets) will be yet performed
The third instruction "i++", is the change of i variable to perform for each cycle of the for.

For instance if we had this for statement:

          for (i =0 ; i < 10 ; i++) {
                    SendToLog("Value of i = %d", i);
          }


We'll have in our log, caught by tomb4_log.exe program, following texts:

Value of i = 0
Value of i = 1
Value of i = 2
Value of i = 3
Value of i = 4
Value of i = 5
Value of i = 6
Value of i = 7
Value of i = 8
Value of i = 9

Note: SendToLog() function, print a text to log file that you can see using the utility tomb4_log.exe. The "%d" will be replaced with the numeric value of parameter typed after the constant message , after first comma.


Using TRNG triggers from our code

Now we have the indices of all teeth-spikes item of current room.
We can manage everyone of them with this code


We should enable all these theeth -spikes.
To to this we can use this action trigger.


Now clicking on [Export Function] button we'll get this export report:

; Set Trigger Type - ACTION 43
; Exporting: TRIGGER(43:0) for ACTION(52) {Tomb_NextGeneration}
; <#> : TEETH_SPIKES ID 52 in sector (1,3) of Room5
; <&> : Trigger. (Moveable) Activate <#>Object with (E)Timer value
; (E) : Timer= +00

PerformActionTrigger(NULL, 43, 52 | NGLE_INDEX, 0);

We can use the PerformActionTrigger() function, to enable all teethspikes.
We should only remove the pre-set index (52 in this case) and replace it with our "Index" variable, that it will contain one by one, all indices of teeth-spikes to enable.
So we'll add this row to our code:

          PerformActionTrigger(NULL, 43, Index, 0);


Note: we removed also the "| NGLE_INDEX" constant, because that flag we'll be used only when we are using an index value got from NGLE program, but now we are using moveable indices taken with Find() function directly from tomb raider game, so they don't require a conversion Ngle->Tomb, because they are already in native format of tomb raider game.


Final code of CeilingTeethSpikes


void CeilingTeethSpikes(void)
{
          int CeilingDistance;
          int LaraTopY;
          int i;
          int Index;

          Get(enumGET.LARA, 0,0);

          Get(enumGET.ROOM, GET.pLara->Room,0);

          Get(enumGET.ITEM_COLL_BOX, GET.LaraIndex, 0);

          LaraTopY = GET.pLara->CordY + GET.pCollItem->MinY;

          CeilingDistance=AbsDiffY(LaraTopY, GET.pRoom->OrigYTop);
          
          if (CeilingDistance <= 64) {
                    // enable all teethspikes of current room, placed on the ceiling
                    Find(enumFIND.ITEM, enumSLOT.TEETH_SPIKES, GET.pLara->Room, 0, -1, NULL);
                    for (i=0 ; i < FIND.TotItems ; i++) {
                              // take [i]th index
                              Index = FIND.VetItems[i];
                              // now in Index there is the index of [i]th teeth-spikes
                              // we trigger it using action trigger 43 with our Index variable:
                              PerformActionTrigger(NULL, 43, Index, 0);
                    }
          }
}


Testing CeilingTeethSpikes()

Before testing our new feature we have to call it from cbCycleBegin()
Since the rooms where there are Teeth-spikes on ceiling to enable are those with indices: 2,3,4 and 5, we'll add in cbCycleBegin() a code to detect when lara is in the right room and call our function only in that circustance.

          if (Room == 2 || Room == 3 || Room == 4 || Room == 5) {
                    CeilingTeethSpikes();
          }

note: since we had already the code to get room of lara and copy that room index in our Room variable (we did that for DangerousWalls()) now we can only check the already set Room variable
At end our cbCycleBegin() function will have this look:



Now build the project (F7 key) and copy plugin.dll in trle folder and play!
We have to verify if the teethspikes will be enabled when lara is touching with her hands the ceiling and, about last room at 5th floor, we have to verify also that when she touches the ceiling ONLY the teethspikes on the ceiling were enabled while the others, on the floors should be not.


Ok, it seems that it works enough fine.


Homeworks

Ok we could say that we completed our three new features: Magic Flare, Dangerous Walls and Ceiling Teeth-spikes.
However there could be some improvement and bug fixing to do but I'll to do to you these improvements , as homework.

You could fix following stuff:

  1. In cbCycleBegin() function we checked the room number to enable special features but what could it happen if we are not in first level but in second or third? In the room 2,3,4 or 5 of this second/third level we'll have dangerous walls in spite they are missing.
    So you should perform a further condition to call our special function, checking if LevelIndex is even 1. In this case we'll perform above function, otherwise we'll not.
    We can get the index of current level with Get(GET.INFO_GAME,...) and you'll find the value in GET.GameInfo.LevelIndex variable.

  2. About dangerous walls we used the value 128 as lower distance to injury lara and it seems that it worked fine but... only when lara is in stand-up position. When she is moving on all fours, she can touch with her head the wall with no damage.
    So you should work on collision box of lara to discover the max width and then using the half of this value instead by using a fixed 128 units.

  3. About magic flare flying...
    Try to discover what's happen when you are flying, then you'll hit DOWN to do move down lara very slowly, now, before touching the ground, save the game and then reload it.
    What's happened? That lara now is moving up insteady by falling down like she was doing when you saved the game.
    The reason is that the value of MyData.FlareVSpeed variable (that affects the up/down movemements) has not been saved in savegame and so when you reload the game the value in MyData.FlareVSpeed variable will be set newly to default -40 value and with this value Lara will move up.
    You have to force the saving and restoring to/from savegame for this variable and you can get this result simply moving its declaration from current MyData structure:

    typedef struct StrMyData {
              StrSavegameData Save; // variable that it will be saved and restored to/from savegame

              int FlareVSpeed; // <----- it's here now

    }MyDataFields;

    to StrSavegameData structure, since that all variable in that structure will be saved and restore from/to savegame.
    Inside of StrSavegameData structure:

    typedef struct StrSavegameData {
              StrSavegameGlobalData Global;
              StrSavegameLocalData Local;
    }SavegameDataFields;

    You have to choose if place it in Global (StrSavegameGlobalData structure) or local (StrSavegameLocalData ) structures.
    Global will work for all levels, while "local" only for current level.
    I suggest to use "Global" since lara could flying from one level to another.
    So we'll declare it in StrSavegameGlobalData structure:

    typedef struct StrSavegameGlobalData {
              // FOR_YOU:
              // define here your variables that you wish were saved (and then restored) to/from savegame in GLOBAL section (only one for all levels)
              // note: the size of this structure should be always even (if you add BYTE variable, compensate it with another BYTE vairable or placefolder)

              int FlareVSpeed; // <---- now you moved it here
    }SavegameGlobalDataFields;

    At end you'll have also to change the path of FlareVSpeed variable, because while first it was in:
    MyData.FlareVSpeed

    now it will be in:

    MyData.Save.Global.FlareVSpeed

    So, everywhere you used it in the source we'll have to replace the first path "MyData.FlareVSpeed" with the second "MyData.Save.Global.FlareVSpeed"






Exercise 2: Lost in Space

This exercise begins in room 8 of plugins project

In this exercise we'll learn how to detect the position in 3d space of floor, walls and ceiling, to move us avoiding all these obstacles.
But before beginning we should find a better way to launch our code about new skills or features.
The previous method we used for Magic Flare it's not very satisfying because we have hardcoded those features checking the room number but also adding a check for level number, what will it happen when we'll use our plugin with another adventure where there will be another level 1, or what should we do, if we wish add red wall feature to other different rooms?
A better way it's to have the chance to enable or disable skills and features with setting from script or NGLE program.
Also for our next exercises it could be useful to be able to perform some code only when we trigger something in game, rather to have it always enabled from code.
A easy way to enable something from the game is ... a trigger.

We have already many trng triggers but now we need of our own trigger, to enable new skills from our plugin.
So as first step we have to create a new trigger, whereby that we'll be able to enable given parts of code everytime we wish.

How to create a new Trigger


In this exercise we'll explain the basics to add new custom triggers to our plugin.
Anyway if you wish study in depth how to add new triggers you can read the tutorial about Creation of new Triggers



The TRG file

To create your new triggers you have to pass to NGLE (room editor) some infos about the description of the trigger: its internal number and its further arguments.
All these data will be typed in a text file with extension ".trg" (like .TRiGger) and with the same name of your plugin.
In spite that its extension it's not ".txt" (warning to avoid to save it as a common text file) the content will be only text.
For instance, the file name for trigger file of "Plugin_Trng", it will be: "Plugin_trng.trg"
To be read from NGLE program, it's necessary that your TRG file was in trle folder.

Description of the trigger

A trigger, in ngle syntax, will be described like a number (the number of that trigger, like flipeffect 20) and its testual description.
You type a new trigger in .trg file following this syntax:

NumberOfTrigger:Description of trigger

Using the colons ":" character to divide number from its description.
For instance:

100:Flipeffect to do explode the level

Above it could be the flipeffect 100 and its description.

Sections of TRG file

Since there are three different trigger types: flipeffects, actions and condition triggers, we'll have to type their description in different sections.
If we wish add a new Flipeffect trigger we have to type its description inside this section:

;------------------ Section for flipeffect trigger: description of the trigger -------------------------------
<START_TRIGGERWHAT_9_O_H>

<END>


While when we add a new Action trigger, we'll type its description in following section:

;------------------- Section for Action triggers: description of the trigger -------------------------------------
<START_TRIGGERWHAT_11_T_H>

<END>


And finally, to add a new Condition trigger, we'll write its description in this section:

;------------------- Section for Condition triggers: description of the trigger -------------------------------------
<START_TRIGGERTYPE_12_T_H>

<END>


Our first flipeffect

We could create a flipeffect trigger with a generic target: give to us the chance to perform some experiment in game when the trigger it will be enabled.
Since the valid range about number of flipeffect trigger, is 0 / 1023, we'll use a number bigger than those already used by trng engine, for instance "800".

Please note that this is not necessary, simply we chose in this way to be sure that when we'll speak about F800, it was clear that we are meaning about our plugin flipeffect trigger.
If we had used a lower number for flip, like 64, we should everytime explain that we are speaking about pugin flip 64, to distinguish it by the F64 trng trigger to print extra ng string on screen.

Now we type the number and description of our new flipeffect trigger in right section of TRG file:

;------------------ Section for flipeffect trigger: description of the trigger -------------------------------
<START_TRIGGERWHAT_9_O_H>
800:Experiments. Perform <&>experiment passing to it the (E)Value
<END>

With above description we set the our F800 trigger, we'll have two arguments.
The "<&>" argument it will be the number of experiment, while the "(E)" argument it will be a parameter that we could pass to some experiment to customize furtherly it.

How to declare the Arguments used by our Trigger

Also the possible values of arguments have to be declared in TRG file, and they require same syntax: "Value:Description:", the only difference is that we have to type these description in a different section.
The section where type them, it will have in its name the number of the trigger whom owns these arguments.
In following table you can see the name of sections for all arguments, remember that where you see "nn" text, you should replace that text with the number of trigger that own that argument, so in our case, it will be the number "800"
To remember the position of trigger fields look also the Set Trigger Type window of NGLE:



Sections to use for Arguments

In this table the "nn" it should be replaced with the number of trigger whom you are typing its argument.
Argument Type FLIPEFFECT ACTION CONDITON
(Object to trigger <#>) ... START_ACTION_nn_O_H
Example:
<START_ACTION_43_O_H>
START_CONDITION_nn_O_H
Example:
<START_CONDITION_14_O_H>
Timer (Parameter <&>) START_EFFECT_nn_T_H
Example:
<START_EFFECT_47_T_H>
... ...
(E)xtra START_EFFECT_nn_E_H
Example:
<START_EFFECT_48_E_H>
START_ACTION_nn_E_H
Example:
<START_ACTION_43_E_H>
START_CONDITION_nn_B_H
Example:
<START_CONDITION_14_B_H>



The declaration of our Flipeffect 800

Finally, this it will be the declaration for our new trigger for experiments:

;------------------ Section for flipeffect trigger: description of the trigger -------------------------------
<START_TRIGGERWHAT_9_O_H>
800:Experiments. Perform <&>experiment passing to it the (E)Value
<END>

;type here the sections for arguments used by above flipeffects
<START_EFFECT_800_T_H>
1: Move the cube
<END>

<START_EFFECT_800_E_H>
#REPEAT#Value=#0#127
<END>


As "Experiment" type, we have only "Move the cube", then we'll add other experiments in next exercises.
While for (E)tra argument, we have a generic list of values, from 0 to 127, to pass to our experiment in the case we need to customize furhterly some experiment.
Anyway we have to explain this special syntax about #REPEAT# tag.


How to create auto-list of arguments. The #REPEAT# tag

The general syntax of repeat command is divided by "#" characters.

#REPEAT#TextToShow#FirstValue#LastValue#StepIncrease

TextToShow field
-----------------------
Is the text that it will be used as description for each value. In this description will be showed also the growing number, from FirstValue to LastValue

FirstValue field
--------------------
This is first value, lower value, for this argument.
The value will be used as argument value but also it will be printed in the description

LastValue field
--------------------
This it will be last value of the sequence.
It is enclosed in the sequence

StepIncrease field
------------------------
This is an optional field, you can also omit it.
The step is a number different of 1 when you do not wish a growing by 1 but with other values.
For instance if you wish arguments like these: 0, 4, 8, 12, 16, 20
You'll use this #repeat# tag.

#REPEAT#SomeText#0#20#4

When you omit the StepIncrease the values will be increased by 1


Our trigger in NGLE program

Now we have to verify if our new trigger will be loaded correctly from NGLE program.
So, we hve to save the TRG file in trle folder and then launch the NGLE program.
Now from NGLE, load a project, and open the Set Trigger Type window.





Click on [Find Trigger Number] button and type "F800"
Probably you'll see only white data for this flipeffect but it is because you have not set your plugin as engine.
Now select your plugin name in "Plugin/Engine" field...


Now you should see above data.
Our new flipeffect 800 trigger has been loaded in NGLE program.

How to catch the activation of our triggers in game

Now we'll place our new trigger in some sector and we'll build the level, with [Exit and Play] button.
But there is yet no code to manage our flipeffect 800...
So, close the game, and start Visual Express 2010, we have to catch when our trigger will be activated.
Now load the plugin sources and then move in "plugin_trng.cpp" source and choose the cbFlipEffectMine() function.
This function will be called from trng engine, everytime a flipeffect trigger (of yours) will be activated in game.
The arguments of this function will give you the info about what is the flipeffect number and the values of its parameters.


int cbFlipEffectMine(WORD FlipIndex, WORD Timer, WORD Extra, WORD ActivationMode)

Since there is only one callback that it will be used to manage all our flipeffect triggers, we have to check the value of FlipIndex argument to perform the right code in according with the current flipeffect.
Our cbFlipeffectMine() function is currently almost empty:

// this procedure will be called everytime a flipeffect of yours will be engaged
// you have to elaborate it and then return a TRET_.. value (most common is TRET_PERFORM_ONCE_AND_GO)
int cbFlipEffectMine(WORD FlipIndex, WORD Timer, WORD Extra, WORD ActivationMode)
{
          int RetValue;
          WORD TimerFull;

          RetValue = enumTRET.PERFORM_ONCE_AND_GO;
          // if the flip has no Extra paremeter you can handle a Timer value with values upto 32767
          // in this case you'll use the following TimerFull variable, where (with following code) we set a unique big number
          // pasting togheter the timer+extra arguments:
          TimerFull = Timer | (Extra << 8);

          switch (FlipIndex) {
                    // here type the "case Number:" for each flipeffect number. At end of the code you'll use the "break;" instruction to signal the code ending
          case -1:
                    // note: remove this "case -1:" and its "break;" it has been added only to avoid warning messages about empty switch
                    break;
          default:
                    SendToLog("WARNING: Flipeffect trigger number %d has not been handled in cbFlipEffectMine() function", FlipIndex);
                    break;
          }

          // if there was the one-shot button enabled, return TRET_PERFORM_NEVER_MORE
          if (ActivationMode & enumSCANF.BUTTON_ONE_SHOT) RetValue= enumTRET.PERFORM_NEVER_MORE;
          return RetValue;
}

Now we'll place our code for flipeffect 800 inside of switch(FlipIndex) { } brackets, beginning with an instruction "case 800:" and ending with "break;" instruction.
All the code we'll put inside of the pair of instruction it will be performed everytime a flipeffect 800 (of ours) it will be triggered in game.

Note: the comments in cbFlipEffectMine() function, suggest us to remove the (placefolder ) "case -1:" instruction, since it has been typed only because it's not possible having an empty switch() statement, anyway we can also replace the "case -1:" with the case for our first flipeffect "case 800:", getting same result:

          switch (FlipIndex) {
                    // here type the "case Number:" for each flipeffect number. At end of the code you'll use the "break;" instruction to signal the code ending
          case 800:
                    // Experiments. Perform <&>experiment passing to it the (E)Value
                    break;
          default:
                    SendToLog("WARNING: Flipeffect trigger number %d has not been handled in cbFlipEffectMine() function", FlipIndex);
                    break;
          }

We placed also a comment row to remember what is this flipeffect 800. This is a good rule to have self-referenced code.

Moving the Cube

Moving an animating in some direction is a feature already supported by many trng triggers, anyway it's important that you understand the backstage and how to peform with your code this skill since in this way you'll be able to perform many other advanced skills.
For this reason, to reach our target, we'll not use other trng triggers, of course.


To jerk or not to jerk?

As first attempt we'll do the easier operation: to move immediatly the cube by one sector to east (ngle) direction.
Our cube has a ngle index = 64.
So we'll type the code to move it to east.

          case 800:
                    // 800:Experiments. Perform <&>experiment passing to it the (E)Value
                    Get(enumGET.ITEM, 64 | NGLE_INDEX,0);
                    GET.pItem->CordZ += 1024;
                    break;

A short code...
We get the structure of item with ngle index = 64:

                    Get(enumGET.ITEM, 64 | NGLE_INDEX,0);

And then we have increased "+=" the value of Z coordinate of 1024 game units (1024 = one sector).

                    GET.pItem->CordZ += 1024;

Since the east (in ngle) is the (growing) direction of Z axis, now the cube should be moved one sector at east.
Now build the project, and copy the .dll in trle folder.
Once in game enter in Exercise 2, and move lara over the blue texture, it is that with the flipeffect 800.

Verification of jerky movement of the Cube



Ok, it works.
Theoratically if lara move out of the blue sector and then she enters newly in the sector, the cube will move yet of another sector at east.
This is happens because we have not used the "ONE-SHOT" button in the trigger, and we let as returned value the enumTRET.PERFORM_ONCE_AND_GO constant.


How to get a gradual movement

In Exercise 1, with magic flare effect, we did move lara up/down and in some direction in gradual way, but in this case our target is more complicated to reach,because in that case the management of Lara object was handled by tomb engine and we changed only some value about horizontal or vertical speed.
Differently now we have to manage ourself the cube object.
To have a gradual movement we should add a little value to Z coordinate (said "horizontal speed") at each frame and for a given number of frame and then stop the movement (and our code).
To reach this target we should solve following issues:

  1. Where should we type the code to perform this movement for many frames?
    The answer: "in our flipeffect 800 code", it's not right, because this code will be performed only once.
    It's true that we could change the returned value of cbFlipEffectMine() function using an instruction:
              return TRET_PERFORM_ALWAYS;
    And in this way, until lara stands on that blue sector, the code will be performed over and over, but... perhaps it's not this that we wished.
    We wished trigger the movement of the cube and that it will continue also if Lara lets that sector immediatly.

  2. How can we remember the original position of the cube, before our movement begun? Because we need to know an info like this to discover the distance we have already performed and to know when to stop the movement (when the covered distance reached the 1024 units)
    It's true that we could use also another method: to compute in advance the number of frame to cover that distance, in according with the used speed, and then to check how much frame it have already elapsed to stop to right frame.
    But also in this case we'll have to save in some global variable these values: the number of frame required and those already elapsed, or the original position of the cube.

Above issues are very common for a lot of features: all those that will be enabled by a trigger and that require many frames to be completed.
To handle in standard way these situations it has been created a special feature named: progressive action.

The Progressive Actions

The progressive action management borns to solve above issues.
You can ask a new progressive action for your targets, and it will be a structure (that groups some global variables) where you can store coordinates or frames (to pass or elapsed) or other custom values you wish.

Another service supplied by progressive action management is to perform the code required for your target for all time (frames) you wish.
Another advantage to use progressive action, rather a self-made method, it's that all variables of progressive actions will be saved to savegame and restored from savegame in transparent way and for this reason your progressive action it will work also in save and load game phases, without you need to worry about that.

Get a new Progressive Action

You require a new progressive action with this code:

                    Get(enumGET.PROGRESSIVE_ACTION,0,0);


Note: the ",0,0" values, in above code, are not meaningful, the enumGET.PROGRESSIVE_ACTION procedure, doesn't require any index, it will be the trng engine to choose a free record and return to you that record. You no need to set or to know the index of returned progressive action.

Now you can initialise your new progressive action with a code like this:

                    // set the kind of progressive action:
                    GET.pAction->ActionType = AXN_MOVE_CUBE;
                    // compute the number of frame required to move the cube for 1024 distance with a speed of 32 game units:
                    GET.pAction->Arg1 = 1024 / 32;
                    // store the speed that we will use:
                    GET.pAction->Arg2 = 32;
                    // remember the ngle index of the cube to move:
                    GET.pAction->ItemIndex = 64;

I understand that it is yet complicate to uderstand but now we'll explain better...

A progressive action is a sort of Organizer script command.
You use an Organizer to do happen some "actions" for a given number of frames.
Well, a progressive action is the same, only that the syntax is different, of course


The structure of Progressive Actions

A simplified version of the structure of a progressive action is the following:

typedef struct StrProgressiveAction {
          WORD ActionType; // AXN_ type
          short ItemIndex; // index of item to manage
          WORD Arg1;           // durate of the action: number of frames or 0xffff for endless
          WORD Arg2; // variable for customizable targets
          int VetArg[6]; // 6 numbers of int type (32 signed bits)
}ProgressiveActionFields;

Later we'll explain better because this is only a "simplified" version, anyway now try to understand above structure.
All above variables are "yours".
You should use those you wish to store all data you need to perform you progressive effect.
The only variable that has a fixed target is the field "ActionType".
When this variable has a value different than 0 (or the mnemoni constant: AXN_FREE), it will be managed by trng engine and "passed" to the "PerformMyProgrAction()" function that you find in "plugin_trng.cpp" source.


Declare new AXN value

In above example we set as action type the value AXN_MOVE_CUBE:

                    // set the kind of progressive action:
                    GET.pAction->ActionType = AXN_MOVE_CUBE;

But this mnemonic constant doesn't yet exist, of course.
We have to declare it in "Constants_mine.h" source, better if in a row closed to the only one AXN_ value already present: the AXN_FREE.
So we'll add our new constant for our progressive action:

// type here the constant name for new progressive action you create
// use progressive number to have always different value for each AXN_ constant/action

#define AXN_FREE 0 // this record is free. You type this value in ActionType to free a action progress record
#define AXN_MOVE_CUBE 1

About the value to assign, just it was different from 0 and from other values used for AXN_ constants. So we chose "1", and when we'll create new AXN_ constants we'll choose "2", "3" ect.

The chameleonic Structure for Progressive Actions

Before continuing, we have to explain because the previous declare of StrProgressiveAction was only a simplified version:
typedef struct StrProgressiveAction {
          WORD ActionType; // AXN_ type
          short ItemIndex; // index of item to manage
          WORD Arg1;           // durate of the action: number of frames or 0xffff for endless
          WORD Arg2; // variable for customizable targets
          int VetArg[6]; // 6 numbers of int type (32 signed bits)
}ProgressiveActionFields;

The "int VetArg[6];" vector are a serie of six numbers. Each of this has 32 bits, and so we can store numbers in the range: -2,147,483,648 / +2,147,483,647
When we'll have to store very big integer numbers, we'll use one of these vector cells.
But what's happen if we need of more than 6 number but it's not necessary that they were so big?
In that case we can use VetShort[12]; vector where the numbers are 12, the double, but they will be littler than int. Each short has a range: -32768 / +32767
And whether we need to store a decimal point number, like "3.1435883"?
In that case we'll use the VetFloat[6] vector. Yet 6 numbers but now they are not "int" (integer) but "float", floating point values with decimal point.

To see the same six numbers serie in different way, it has been used the "union" class type.
You can have a full description of "union" and structure declaration in Basic of C++ language: the Structure declaration help file.


Anyway it's not important you understood how the "union" works, just only you remember that you have different alternative names to access to variable of structure of progressive action.
In according with the type of values you need, you'll use a name or another.
In this situation it's important you avoid to get a conflict, using two different variables that have same storage position.


In left column you see the name of different variables, while at right side there is the Storage Map.
To avoid conflicts you should see this storage map like it was divided by columns.
Everytime you use a variable in a given position, you should check that you are not using also another variable that is in same column.
For instance, if you use the variable:

VetArgWord[0];

You cann't use, in same action, the variables that stand on same columns, and so you cann't use following variables:

VetArgBytes[0];
VetArgBytes[1];
VetArgSignedBytes[0];
VetArgSignedBytes[1];
VetArgShort[0];
VetArgDword[0];
VetArg[0];
VetArgFloat[0];
From.OrgX;
Rect.left

Because all above variables are in same column of VetArgWord[0];

The real declaration of StrProgressiveAction structure

Now we can see the real declaration of structure for progressive action, where we can see also all different names:

typedef struct StrProgressiveAction {
          WORD ActionType; // AXN_ code          
          short ItemIndex; // Index of item to manage
          WORD Arg1; // durate of the action: number of frames or 0xffff for endless
          union {
                    WORD Arg2; // variable for customizable targets
                    StrTwoBytes Bytes;
          };
          union {
                    int VetArg[6]; // 6 numbers of int type (32 signed bits)
                    float VetArgFloat[6];
                    WORD VetArgWord[12];
                    short VetArgShort[12];
                    BYTE VetArgBytes[24];
                    char VetArgSignedBytes[24];
                    DWORD VetArgDword[6];
                    StrDoublePosition Coords;
                    StrRectXY Box;
          };
}ProgressiveActionFields;


You can see the "union" with different names for same storage space.
We see that there is an "union" also for "Arg2" field, but in this case the chances are only two.
To use Arg2 for a word value, or two bytes (See image at left).



To start our progressive action to move the cube

Now we come back to our target.
We have to move the cube at east of 1024 game units in soft way.
To realize this target, when the flipeffect 800 will be engaged, we'll start a progressive action to move the cube.
So we get a new progressive action record and we'll initialise its variables in this way:

          case 800:
                    // 800:Experiments. Perform <&>experiment passing to it the (E)Value
                    Get(enumGET.PROGRESSIVE_ACTION,0,0);
                    GET.pAction->ActionType = AXN_MOVE_CUBE;
                    GET.pAction->Arg1 = 1024 / 32;
                    GET.pAction->Arg2 = 32;
                    GET.pAction->ItemIndex = 64;
                    break;

With above code (placed in cbFlipEffectMine() callback), we started the progressive action.
But now, where we'll type the code to perform our progressive action?

The PerformMyProgrAction() function

All our progressive actions will be performed, frame by frame in game cycle, from the PerformMyProgrAction() function.

// this function will be called for each your (common) progressive action to be peformed
void PerformMyProgrAction(StrProgressiveAction *pAction)
{
          switch (pAction->ActionType) {
                    // replace the "case -1:" con your first "case AXN_...:" progressive action to manage)
          case -1:
                    break;
          }
}

In the main "switch()" of this function, we'll type our new action type, with usual instruction "case Value:", and in that point we'll type our code to perform that progressive action.
So, since our new progressive action has the id-type AXN_MOVE_CUBE, we'll change above code in following way:

// this function will be called for each your (common) progressive action to be peformed
void PerformMyProgrAction(StrProgressiveAction *pAction)
{
          switch (pAction->ActionType) {
          case AXN_MOVE_CUBE:
                    // move the cube at east
                    break;
          }
}

So, now we'll type our code after the "case AXN_MOVE_CUBE:" instruction.
Remembering newly the values we saved in our progressive action:

GET.pAction->ActionType = AXN_MOVE_CUBE;
GET.pAction->Arg1 = 1024 / 32;
GET.pAction->Arg2 = 32;
GET.pAction->ItemIndex = 64;

We had:
Arg1 = Number of frames for this progressive action.
Arg2 = Speed, i.e. the value to add to Z coordinate for each frame of the durate.
ItemIndex = the ngle index of our cube object
We'll find newly these values in pAction pointer structure.
So as first point we'll check if we have already completed the progressive action, checking if number of frame (Arg1) reached 0.

                    if (pAction->Arg1 == 0) {

In the case it is 0, it means we completed our progressive action and so we'll free it:

          case AXN_MOVE_CUBE:
                    if (pAction->Arg1 == 0) {
                              // completed! disable and free this progressive action
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    break;

While if the frame yet to perform is not 0, we'll have to move the cube, but, first of this, we should remember to decrease the number of frames already performed by 1
In this situation we should also remember the case that in some circustances it could be true: the case when the number of frame is "endless", in that case the value of frames will be even at constant ENDLESS_DURATE (value 0xFFFF or 65535). If the number of frame is ENDLESS_DURATE we don't change it, and so before decreasing the frame value we should first check if number frame is ENDLESS_DURATE, and to decrease it only when it is not that special frame value.

                    // decrease performed frames
                    if (pAction->Arg1 != ENDLESS_DURATE) {
                              pAction->Arg1--;
                    }

Now we have to get the structure of our cube item, and since we know that its index it has been saved in ItemIndex variable, we'll type this code:

                    Get(enumGET.ITEM, pAction->ItemIndex | NGLE_INDEX, 0);

Now in GET.pItem we have its structure so finally we can change its z coordinate simply adding to its Z coordinate the speed value, saved in Arg2 variable of our progressive action structure.

                    GET.pItem->CordZ += pAction->Arg2;


The final code for AXN_MOVE_CUBE progressive action

So this it will be the final code to manage our progressive action:

          case AXN_MOVE_CUBE:
                    if (pAction->Arg1 == 0) {
                              // completed! disable and free this progressive action
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    // decrease performed frames
                    if (pAction->Arg1 != ENDLESS_DURATE) {
                              pAction->Arg1--;
                    }
                    Get(enumGET.ITEM, pAction->ItemIndex | NGLE_INDEX, 0);

                    GET.pItem->CordZ += pAction->Arg2;
                    break;

Now we have to try it.
Build the projet (F7), copy the .dll from debug folder to trle folder and launch tomb4 program.




Ok, it works.
Anyway if we try to move lara in and out from blue sector to have many movements of the cube we see that the cube ignores the room collisions: pass over the floor slope but, the worse, it goes throught the wall and disappears behind of it.


Improvement of the Cube movements: detection of room collisions

Now we try to analise where the cube is moving, to verify if it is possible the current movement.
More, we could change also its dynamic.
Insteady by moving of a fixed size in a fixed direction, we could to do move it in endless way, at least until it find an obstacle that it will stop it.

We have to do some changes to our code.
In the cbFlipEffectMine() function, we'll type as frame durate (Arg1) the endless constant:

          case 800:
                    // 800:Experiments. Perform <&>experiment passing to it the (E)Value
                    Get(enumGET.PROGRESSIVE_ACTION,0,0);
                    GET.pAction->ActionType = AXN_MOVE_CUBE;
                    GET.pAction->Arg1 = ENDLESS_DURATE;
                    GET.pAction->Arg2 = 32;
                    GET.pAction->ItemIndex = 64;
                    break;

But it will be in the PerformMyProgrAction() function that we'll have to do most changes.

How to detect the height of the floor

To discover the height of the floor in a given point of 3d world (x,y,z) we'll use the CheckFloor() function.

bool CheckFloor(DWORD x, int y, DWORD z, int RoomIndex)

This function requires as arguments the coordinate of 3d point where peform the test: x,y and z, and the room index where that point should fall.
The found infos about floor collisions will be saved in FLOOR global structure.

Note: About RoomIndex argument, it could be wrong when you are checking the floor in front of some item at a given distance from it. Pratically it could be happen that the item was in room 3 and you check what is forward of him of 256 units, but if that enemy was very closed to door for other room (let's say the room 4) you'll supply as input for CheckFloor() the room index of the item (3 in our example) while the point you are checking is really in room with index =4.
This is ok, indeed another target of CheckFloor() is to discover in what room falls a given (x,y,z) point. The real room where the point falls, it will be returned in FLOOR.RoomIndex variable.
Anyway it works until you give as input room index a room that is closed to real room. In the case the room you give is very far from the real room where that point lays you could have bad results.


The FLOOR global structure

In the reality the CheckFloor() function doesn't return only infos about floor but also about ceiling, water depth and height and some other special status about that sector.
This is the declaration of FLOOR structure:

typedef struct StrFloorAnalyse {
          bool TestFullWall; // the point is inside wall: no other result will be meaningful
          bool TestGraySector; // the point is on gray sector forbidden to enemies
          bool TestClimb; // == true if there one wall with climb state around current sector, see ClimbStatus for more info
          bool TestMonkey; // == true monkey ceiling over this sector
          bool TestDeath; // == true death (lava) type on the floor of this sector
          WORD ClimbStatus; // CLIMB_ values or 0 if missing climb walls in current sector
          int FloorHeight; // Y Coordinate of floor (higher side)
          int CeilingHeight; // Y coordinatre of ceiling (lower side)

          int WaterDepth; // Depth of the water in the vertical of input point
          int WaterHeight; // y coordinate of water surface
          short RoomIndex; // the room where is the given point
          WORD *pFloor; // pointer to floor data about point, to use as input for other low-level functions
          int SlopeType; // SLOPE_ values to describe if the sector is flat or a slope and what kind
          int SlopeX; // click difference about height on X axis on higher X values (south side on ngle view)
          int SlopeZ; // click difference about height on Z axis on higher Z values (east side on ngle view)
}FloorAnalyseFields;

Most of above variables are easily understandable, anyway we have to explain better those more specific.


bool TestFullWall

When (after a call to CheckFloor() function) the value in TestFullWall is = true, this means that the point you supplied as input it was inside of some wall.
In this situation other variables will have no sense, and you should understand simply that it's not possible move to that point.
Note: the CheckFloor() function returns own this value.
This means that you can check in fast way if there is a wall also in this way:

          if (CheckFloor(x,y,z RoomIndex) == false) {
                    // in this position there is a wall: no further analysis have sense
          }


Slope types for Floor

The "SlopeType" variable uses SLOPE_ constant values:

#define SLOPE_FLAT 0 // no slope, all corners have same height
#define SLOPE_GENTLE_SLOPE 1 // slope (1 or 2 clicks) on a single side where lara is able to walk
#define SLOPE_STEEP_SLOPE 2 // slope (3 clicks or higher) on a single side where lara is not able to walk
#define SLOPE_GENTLE_CORNER 3 // triangle slope (1 or 2 clicks) on a single corner where lara is able to walk
#define SLOPE_STEEP_CORNER 4 // triangle slope (3 clicks or higher) on a single corner where lara is not able to walk

To understand the direction of the slope you have to check the "SlopeX" and "SlopeZ" variables.


Looking above image you can see as the SlopeX and SlopeZ will been gotten in according with SlotType.
About the SLOPE_FLAT value, it means that there is no slope on that sector.

The new code to detect floor height

In the PerformMyProgrAction() function we'll place a code to check the point in (x,y,z) coordinates where the cube should move.
We have to perform this compute in advance, before to have really moved the cube in that position.
For this reason we'll compute those that should be the new coordinates, verifying if in that point there is a flat (no slopes) sector with same floor height of current Y coordinate of the cube.

void PerformMyProgrAction(StrProgressiveAction *pAction)
{
          int NewZCord;

          switch (pAction->ActionType) {
                    // replace the "case -1:" con your first "case AXN_...:" progressive action to manage)
          case AXN_MOVE_CUBE:
                    if (pAction->Arg1 == 0) {
                              // completed! disable and free this progressive action
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    // decrease performed frames
                    if (pAction->Arg1 != ENDLESS_DURATE) {
                              pAction->Arg1--;
                    }
                    Get(enumGET.ITEM, pAction->ItemIndex | NGLE_INDEX, 0);
                    NewZCord = GET.pItem->CordZ + pAction->Arg2;
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, NewZCord, GET.pItem->Room);

We have declared a local variable, NewZCord, to save the value of Z coordinate where the cube should move.
Then we called the CheckFloor() function, giving as argument that it should be the new position of the cube after to have added the speed.

                    NewZCord = GET.pItem->CordZ + pAction->Arg2;
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, NewZCord, GET.pItem->Room);

If that (future) point there is an obstacle, we'll stop the cube and will close the current progressive action:

                    if (FLOOR.TestFullWall == true) {
                              // cube stopped by a wall
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    if (FLOOR.SlopeType != enumSLOPE.FLAT) {
                              // the next sector is not flat
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    if (FLOOR.FloorHeight != GET.pItem->CordY) {
                              // the next sector has an height different than current Y coordinate of the cube
                              pAction->ActionType = AXN_FREE;
                              break;
                    }

In above code we used three condition about obstacles:
If the next position into a wall.
If next sector is not flat but with some slope.
If the next sector is higher or lower than current Y coordinate of the cube.
If one or more conditions are true, the cube it will be stopped, freeing the current progressive action.

While if no one condition is true, there is no obstacle so we'll change really the coordinate of the cube:

                    // no obstacles: move the cube in next position:
                    GET.pItem->CordZ += pAction->Arg2;


Finally the new code for our progressive action it will be the following:

          case AXN_MOVE_CUBE:
                    if (pAction->Arg1 == 0) {
                              // completed! disable and free this progressive action
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    // decrease performed frames
                    if (pAction->Arg1 != ENDLESS_DURATE) {
                              pAction->Arg1--;
                    }
                    Get(enumGET.ITEM, pAction->ItemIndex | NGLE_INDEX, 0);
                    NewZCord = GET.pItem->CordZ + pAction->Arg2;
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, NewZCord, GET.pItem->Room);

                    if (FLOOR.TestFullWall == true) {
                              // cube stopped by a wall
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    if (FLOOR.SlopeType != enumSLOPE.FLAT) {
                              // the next sector is not flat
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    if (FLOOR.FloorHeight != GET.pItem->CordY) {
                              // the next sector has an height different than current Y coordinate of the cube
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    // no obstacles: move the cube in next position:
                    GET.pItem->CordZ += pAction->Arg2;
                    break;


Now try in game what it will happen...


Not so fine, this time



Ok, the cube stops but it moved too much over.
What's happened?
The reason is that we checked the point of current coordinate of cube + distance of speed, but the current position of cube (its pivot) is in the central position, while we wish to know what is the floor after its east boundary (where the cube has its blue face)




Looking above image is easy understand the problem.
The cube stopped only when its pivot + speed , was over the higher floor (that with the slope)
But we need to know the floor status on the red point (in above image)
To fix our code we'll have to add to speed also the half of width of the cube.
Since we know that the cube is 1024x1204x1024 game units, the half width will be 512, anyway we'll use a bit less, 511 to avoid that speed+512 to do stop the cube a bit too early letting an empty space between cube and obstacle.

So we'll change our code from:

                    NewZCord = GET.pItem->CordZ + pAction->Arg2;

to this:

                    NewZCord = GET.pItem->CordZ + 511 + pAction->Arg2;

In this way we'll check the position of its boundary at east, where it should touch first obstacle.

Now we build and control this new code...




Ok, now it stops in right position


Robotic Random Movemement

Another step to improve our cube it could be to give to it some movement rules to do turn it when there is some obstacle.
A logic of movement it could be the following:

  1. Check if forward sector is free.
    If it is move forward

  2. If it was not free: try to move at left (-90 degrees, west direction respect to its movement)

  3. If at left there is a free way, turn the facing of -90 degrees and start moving in this new direction

  4. If at left there is another obstacle, try to move at right (own east angle, 90 degrees at right)

  5. If at ritght there is free way, turn the facing by +90 degrees, and move the cube to this new direction

  6. In the case no movement it is possible to do explode the cube


Our new function with arguments

We had already created our own functions, like MagicFlare() or DangerousWalls() but in that case we did only to keep orderly our sources, to group some code rows with a meaningful name.
Ok that is a target of functions, but there is another and more important target of the functions...
When we assign input argument to our functions, we can have a same code to apply to different objects or with different modality.
Pratically everytime we are creating code to have a skill or to solve a problem that we could have many other times in the future, we should create a function to have the chance, in the future, to use same code with other objects.
The input arguments of this new function, will be own the object or other input parameter to change in the future to satisfy our future requirements.


Function to detect "Free Way" for our objects

Let's look newly our robotic movement logic:

  1. Check if forward sector is free.
    If it is move forward

  2. If it was not free: try to move at left (-90 degrees, west direction respect to its movement)

  3. If at left there is a free way, turn the facing of -90 degrees and start moving in this new direction

  4. If at left there is another obstacle, try to move at right (own east angle, 90 degrees at right)

  5. If at ritght there is free way, turn the facing by +90 degrees, and move the cube to this new direction

  6. In the case no movement it is possible to do explode the cube

Looking above list it's clear that we'll have to perform almost same computes for three directions: forward, at left (if forward is busy) and at right (if also at left it's busy)
These computations will be the checking of floor height, and the control about slope or wall in that sector.
In this situation we can create a function that it will receive as input the direction where control (forward, left or right) and the index of moveable.
In this way we'll type the code only once and then we'll be able to use it over and over for all directions and with all moveables.

The IsFreeWay() function

We could name this function IsFreeWay() since its target is own to say us, if it is possible moving in some direction.
As first point we should choose the input parameter of this function.
The variable parameters will be:

  1. The index of moveable. This is necessary to know the source position from where perform our computes about floor collisions

  2. The direction where to check the free way.
    This it will be a relative direction, i.e. relative to current facing of the object. So it could be at (its) right, or at (its) left ect.

  3. The distance from source point (of item) in the given direction, where to check for obstacles.
    In our previous code we used the current speed and it was "32"

About the returned value it will be "true" (the way is free) or "false" (there are obstacles).
When we wish that a variable had only "true" or "false" values, we have to use a boolean variable, and its type name is "bool".
So the declaration of our function it will be:

bool IsFreeWay(int ItemIndex, short Direction, int Distance);


How to work with directions

We had already said that the facing or orienting is pratically a value to set a direction.
Our IsFreeWay() function has like input parameter also "Direction" and it will be a value like a facing to point to east, north, east or west but also in any other direction.
Since the facing value can host 65536 values, this means that each object could look in 65536 different directions.
It's true than for our "cube robot" we'll use only four hortogonal directions but since we are doing a function that it could be used also for other moveables, it's better try to manage the analyse for ANY direction from source point.


Looking above image we see that it's easy discover the point that stand to 2 sectors (2048 units) from SAS guy, in hortogonal direction.
The A and B point can be discovered simply adding 2048 to Z coordinate (A point) or to X coordinate (B point)
But if we want discover the point to same distance (2048) but placed at south-east direction the matter is more complicated.
Repeating previous compute, adding 2048 to both X and Z coordinates will get the C point, but it should be in a wrong position: it is more far than 2048 units.

The right position should be that of D point in above picture, but how can we compute it?

To get the real position of any point with any distance and direction we'll have to use the GetIncrements() functions.

The GetIncrements() function


void GetIncrements(WORD Facing, int *pIncX, int *pIncZ, int Distance);

In spite in this case the argument has name, "Facing", it is always the same: a direction.
Above function requires as input the direction (Facing) and the distance (Distance) and it will return two values: XIncrement and ZIncrements.
These two values, once added to source position (for example the x and z coordinates of some item) they will give the coordinates of the new point placed at required distance and direction.

Arguments of Input or of Output?

In this function we discover another trick of C++ language.
Usually a function is able to return only one value.
In the function body it will be returned with an instruction like:

          return Result;

While in the call of the function, it will be gotten like the function name was the name of a variable
For instance, if there is a function to compute the average of a serie of numbers, we could get the average value in this way:

          Result = GetAverage(14, 345, 20);

And now in Result variable, there will be the value that the GetAverage() function has computed and gave to us with its "return Value;" instruction.
But what happen when we wish that a function returns to us two or more values?
One method it's that to store all output values in a global structure, like it happens with Get() function that saves its results in GET global structure, or the CheckFloor() in the FLOOR global structure, but another method, is to pass as input arguments the addresse of some variable where we wish that the function wrote the returned values.
It's like if you gave to someone some empty boxes and he has to fill them with his work, and then you will have returned them back newly but now with inside something.
This is the case of GetIncrements() function.
A common usage of this function could be the following:

          int IncX;
          int IncZ;

          int NewPosX;
          int NewPosY;
          int NewPosZ;

          // get structure of a moveable item
          Get(enumGET.ITEM, pAction->ItemIndex | NGLE_INDEX, 0);

          // discover the coordinate of 256 game units in front of the item, where it is looking
          GetIncrements(GET.pItem->OrientationH, &IncX, &IncZ, 256);

          // compute the absolute coordinate placed at 256 game units in front of the item
          NewPosX = GET.pItem->CordX + IncX;
          NewPosY = GET.pItem->CordY;
          NewPosZ = GET.pItem->CordZ + IncZ;

Now the 3d point (NewPosX, NewPosY, NewPosZ) it will be that at 256 game units from source position of pItem, in the direction of its current facing, any value it had, not only hortogonal.


The code of IsFreeWay() function

Now we have all tools required to create our function.

bool IsFreeWay(int ItemIndex, short Direction, int Distance)

{
          int IncX;
          int IncZ;
          int NewX;
          int NewY;
          int NewZ;
          short AbsDir;

          Get(enumGET.ITEM, ItemIndex, 0);
          AbsDir = GET.pItem->OrientationH + Direction;

          GetIncrements(AbsDir, &IncX, &IncZ, Distance + 511);

          NewX = GET.pItem->CordX + IncX;
          NewY = GET.pItem->CordY;
          NewZ = GET.pItem->CordZ + IncZ;
          CheckFloor(NewX, NewY, NewZ, GET.pItem->Room);

          if (FLOOR.TestFullWall == true) {
                    // cube stopped by a wall
                    return false;
          }
          if (FLOOR.SlopeType != enumSLOPE.FLAT) {
                    // the next sector is not flat
                    return false;
          }
          if (FLOOR.FloorHeight != GET.pItem->CordY) {
                    // the next sector has an height different than current Y coordinate of the cube
                    return false;
          }
          // no obstacles: the way is free
          return true;
}

The code should be understandable.
We got the structure of item using the index set in input argument ItemIndex

          Get(enumGET.ITEM, ItemIndex, 0);

Then we compute the direction where performing the check of the floor

          AbsDir = GET.pItem->OrientationH + Direction;

Since we wish know what is at left, or right of forward respect to facing of the item, the direction that we'll supply as Direction argument, it will be a relative direction.
For instance to see at left it means - 0x4000 (or -16384) while at right it will be 0x4000 (or +16384). But we wish mean the direction in according with the current facing of the object, so we'll have to add to current facing (forward) direction of the object its left (west) or right (east) side.
For instance if we wish analyse the sector in front of the item, we'll give as Direction the value 0 because it means: don't change the facing of the item and check in that direction.

Then we'll compute the real 3d position of the point to check, using GetIncrements() function and applying the increments to source item

          GetIncrements(AbsDir, &IncX, &IncZ, Distance + 511);

          NewX = GET.pItem->CordX + IncX;
          NewY = GET.pItem->CordY;
          NewZ = GET.pItem->CordZ + IncZ;

Note: in above code we have increased the distance (usual the "speed") with 511 value to compute the half/width of depth of the item, like we had already seen in previous code.
Anyway it will work for our cube but it could be wrong for other moveables with other size.
We'll fix this limitation later

At end we'll have all other code to check result about floor, that is the same code that we had already used in our progressive action, the only difference is that now we'll not use the code to stop progressive action and then "break;" but in this case we'll return the result as "false", because the way is not free.

          if (FLOOR.TestFullWall == true) {
                    // cube stopped by a wall
                    return false;
          }
          if (FLOOR.SlopeType != enumSLOPE.FLAT) {
                    // the next sector is not flat
                    return false;
          }
          if (FLOOR.FloorHeight != GET.pItem->CordY) {
                    // the next sector has an height different than current Y coordinate of the cube
                    return false;
          }


How to use IsFreeWay() function

Now we can change our main code to move the cube, using the IsFreeWay() function to discover the available directions where to do move the cube.
In previous release of our code we kept the index in ngle format and everytime we used it, we added to its value the NGLE_INDEX constant.
Anyway when we use very often a ngle index it should be better convert it to tomb format and then to use this tomb (native) index to save the time for conversion and to avoid the boring to add everytime the NGLE_INDEX constant.
So we begin declaring a new variable (we are in PerformMyProgrAction() function) to save the index of cube in tomb raider format:

          int IndexTomb;

And now we rewrite from scratch the code for our progressive action

          case AXN_MOVE_CUBE:
                    IndexTomb= FromNgleIndexToTomb4Index(pAction->ItemIndex);

Now we'll use always the IndexTomb as index of the cube.

                    Get(enumGET.ITEM, IndexTomb, 0);

We got the structure of the cube.

Now to perform our test in three directions: forward, at left and at right, we need of some variable to store the direction to choose and to remember when previous test about direction had a good or bad result.

          short Direction;
          bool TestOk;

Above declaration will be placed at top of PerformMyProgrAction() function, of course.
In Direction we'll save the chosen direction, while in TestOk we'll keep "true" if we found a good direction with free way, or "false" until we have not yet found a free way.

                    Direction = 0;
                    TestOk=false;

We initialise our variables.
Our first test it will be to move forward (Direction = 0) i.e. with no change of direction respect to the facing (current direction) of the cube.
While we initialise TestOk at "false" because we have not yet found a free direction.

                    // try to continue to move forward
                    if (IsFreeWay(IndexTomb, Direction, pAction->Arg2) == true) {
                              // it is possible continue to move forward
                              TestOk=true;
                    }

Now we used our IsFreeWay() function, passing as direction the current value of Direction variable. It is 0, now, to check in front of the cube (no change about its current facing)
The "pAction->Arg2" variable contains the speed, in this case our distance from source point to check.
If result of IsFreeWay() is true, it means that the cube is able to go on in current direction, so we set "TestOk=true;" to remember that it's not necessary perform test for other directions.

In the case the result was false, also "TestOk" variable will have yet "false" value and we'll have to check if at left of the cube there is free way:

                    if (TestOk==false) {
                              // it was not possible to move forward: try to turn at left
                              Direction= -16384; // (- 0x4000 90 degrees at west);
                              if (IsFreeWay(IndexTomb, Direction, pAction->Arg2) == true) {
                                        // it is possible turn at left
                                        TestOk=true;
                              }
                    }

In above code we verified if we had found free way in previous code.
When "TestOk==false" we had not, so we change Direction to verify if at -90 degrees (at west/left of the cube facing) there is free way.
If IsFreeWay() returns "true", the way is free at left, so we set "TestOk=true" to remember that we have already found a free direction and in Direction variable there will be the last free found direction.

At end we perform same analyse but this time for right direction, i.e. ad right (east) of the cube facing:

                    if (TestOk==false) {
                              // it was not possible move forward and neither turning at left: try to turn at right
                              Direction = 16384;
                              if (IsFreeWay(IndexTomb, Direction, pAction->Arg2)==true) {
                                        // it is possible turn at right
                                        TestOk=true;
                              }
                    }

How you can see, above code it is almost the same of previous, we changed only the value to set in "Direction" variable. In this case we set +16384 the should 90 degrees at east (right).
Now we have performed a check for each of three possible direction.
If "TestOk" variable is "false" this means that the cube has no available direction, so we'll do explode it, using the Action 14:

                    if (TestOk == false) {
                              // it has been NOT possible move in any direction: to do explode the cube
                              PerformActionTrigger(NULL, 14, IndexTomb, 2);
                              // and disable the progressive action for the cube
                              pAction->ActionType = AXN_FREE;
                              break;
                    }

In above code we'll do explode the cube if there was no free direction, and then we'll stop the progressive action and quit the code with "break;"

In the case we reach the following code it means that TestOk was true, we had found a free direction and now we should move really the cube in new direction.
In the case the Direction variable was different than 0, it means we turned the cube at left or at right, so we should change also its facing to do that it moves always in front of it.
Since the blue face is that where the cube is looking, we'll have to change its facing value:

                    // it's possible to do move the cube
                    // if the (relative) direction is different than 0 (0=continue forward), we'll have to change also
                    // the facing of the cube
                    if (Direction != 0) {
                              GET.pItem->OrientationH += Direction;
                    }

After above code we are sure that the cube facing is pointing in that it will be the new (or old, but valid) direction.
So we'll use the GetIncrements() function to discover the new coordinates of the cube, following its current facing and at distance of current speed:


                    // now compute the new position of the item using its facing because we have already changed it if it was
                    // necessary
                    GetIncrements(GET.pItem->OrientationH, &IncX, &IncZ, pAction->Arg2);

                    GET.pItem->CordX += IncX;
                    GET.pItem->CordZ += IncZ;
                    // we have to update room of object in the case it was changed
                    UpdateItemRoom(IndexTomb);
                    break;

In above code we got the increments to move in wished direction and then we add these values to x and z coordinates of the cube to update its position.
At end, we called the UpdateItemRoom() function to update the room index.

Our new robotic Cube code

Our final code in PerformMyProgrAction() function it will be the following:

void PerformMyProgrAction(StrProgressiveAction *pAction)
{
          short Direction;
          bool TestOk;
          int IndexTomb;
          int IncX;
          int IncZ;

          switch (pAction->ActionType) {
          case AXN_MOVE_CUBE:
                    IndexTomb= FromNgleIndexToTomb4Index(pAction->ItemIndex);
                    Get(enumGET.ITEM, IndexTomb, 0);
                    Direction = 0;
                    TestOk=false;

                    // try to continue to move forward
                    if (IsFreeWay(IndexTomb, Direction, pAction->Arg2) == true) {
                              // it is possible continue to move forward
                              TestOk=true;
                    }
                    if (TestOk==false) {
                              // it was not possible to move forward: try to turn at left
                              Direction= -16384; // (- 0x4000 90 degrees at west);
                              if (IsFreeWay(IndexTomb, Direction, pAction->Arg2) == true) {
                                        // it is possible turn at left
                                        TestOk=true;
                              }
                    }
                    if (TestOk==false) {
                              // it was not possible move forward and neither turning at left: try to turn at right
                              Direction = 16384;
                              if (IsFreeWay(IndexTomb, Direction, pAction->Arg2)==true) {
                                        // it is possible turn at right
                                        TestOk=true;
                              }
                    }
                    if (TestOk == false) {
                              // it has been NOT possible move in any direction: to do explode the cube
                              PerformActionTrigger(NULL, 14, IndexTomb, 2);
                              // and disable the progressive action for the cube
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    // it's possible to do move the cube
                    // it the (relative) direction is different than 0 (0=continue forward), we'll have to change also
                    // the facin of the cube
                    if (Direction != 0) {
                              GET.pItem->OrientationH += Direction;
                    }
                    // now compute the new position of the item using its facing because we have already changed it if it was
                    // necessary
                    GetIncrements(GET.pItem->OrientationH, &IncX, &IncZ, pAction->Arg2);
                    GET.pItem->CordX += IncX;
                    GET.pItem->CordZ += IncZ;
                    // we have to update room of object in the case it was changed
                    UpdateItemRoom(IndexTomb);
                    break;
          }
}


Now we have only to try our new code...




Ok, it works like we wished.
The cube turns everytime it finds a not flat sector in front of it, and then it goes into a tunnel where it has no more available directions and so it explodes.
We can see that also its facing (the blue side) changes to be always on same direction of movement.

Moving on the slopes

Another improvement for our cube movements it could be to give to the cube the ability to move over gentle slopes.


In above image we see the two slopes of source room of the cube.
That with red face it is normal that the cube was not able to pass because there is a protrusion of the floor that it should stop its sliding movement.
But the other slope is very soft and with right orienting to accept the cube sliding.
Now we'll change the code to give to the cube the skill to move over one-click slope with correct orienting.
We'll modify the code in IsFreeWay() function, since it's there that we'll check about the floor status for possible new directions.
To understand better the problem we should see newly the picture about different kinds of slope that the CheckFloor() function returns




How to change the IsFreeWay() function to accept slope sectors

Looking the code of IsFreeWay() function we discover the point where we'll have to do some change:

          if (FLOOR.SlopeType != enumSLOPE.FLAT) {
                    // the next sector is not flat
                    return false;
          }

While in old code we returned a "false" result for any not flat sector, now we'll have to do some other test, in that case, to verify if the slope is one of those we could accept.
Our new rule should be the following:

  1. If the sector is a gentle slope

  2. ... and the slope grade is only of one click (not two)

  3. ... and the orienting of the slope is compatible with the direction of the cube

  4. then IsFreeWay() function will accept that sector like "free way"

How we will see, the more complicated stuff is the point number 3 of above list.
Because in that case we'll have to discover if the slope is pointing to east, south, north or west and then compare this result with current direction for our object.
We find the direction of the slope in FLOOR.SlopeOrienting variable that uses ORIENT_ values.

Changing the code of IsFreeWay() function to manage slopes

So we'll start from this side of the code:

          if (FLOOR.SlopeType != enumSLOPE.FLAT) {
                    // the next sector is not flat
                    return false;
          }

To manager the situation where we could find a gentle, hortogonal, slope with correct direction to accept as "free way"

          if (FLOOR.SlopeType != enumSLOPE.FLAT) {
                    // verify if it's a bad slope to refuse
                    if (FLOOR.SlopeType != enumSLOPE.GENTLE_SLOPE) {
                              // this slope is on a single corner or too steep: no free way
                              return false;
                    }
                    // to check that the slope grade was not higher than one click
                    if (FLOOR.SlopeClickGrade > 1) {
                              // too inclinated: no free way
                              return false;
                    }

                    // it is gentle slope, now we have to verify if the direction of the slope is the same of current direction (AbsDir) of our check
                    if (FLOOR.SlopeOrienting != AbsDir) {
                              // the slope has a different direction: no free way
                              return false;
                    }

          }

In above code, we have first checked if the slope type was one of those we cann't accept: in that case we quit immediatly with "return false;"
Then we compared the direction of the slope with the direction we are checking (AbsDir). If these two values are even is ok, otherwise we'll return newly "false"

We had to do another change to the code of IsFreeWay() function:
The old code about verifiy of floor height:

          if (FLOOR.FloorHeight != GET.pItem->CordY) {
                    // the next sector has an height different than current Y coordinate of the cube
                    return false;
          }

It's not good after our changes, because now it's normal that, moving the cube over a slope, the Y coordinate of the floor of next sector coluld be a bit higher than current Y coordinate of the cube.
So we'll change above code to accept a little difference of heigth, that difference that we suppose to be enclosed in a gentle slope:

          // only if the difference of Y coordinates is higher than 200 we'll return false
          // because on a slope it's normal having a difference in Y coordinate but lower than one click
          if (AbsDiffY( FLOOR.FloorHeight, GET.pItem->CordY) > 200) {

                    // the next sector has an height too much different than current Y coordinate of the object
                    return false;
          }

For same reason we have to do a change also in code of our progressive action.
The old code to update the position of the cube:

                    GetIncrements(GET.pItem->OrientationH, &IncX, &IncZ, pAction->Arg2);

                    GET.pItem->CordX += IncX;
                    GET.pItem->CordZ += IncZ;
                    // we have to update room of object in the case it was changed
                    UpdateItemRoom(IndexTomb);
                    break;

It's missing of change about Y coordinate, that it's necessary, since moving the cube over a slope its Y coordinate will change, remaining the same of the floor but over a slope it will be higher.
So we have to add this code:




                    // in the case the cube was over a slope we have to change also its Y coordinate, setting it to same
                    // value of floor height
                    GET.pItem->CordY = FLOOR.FloorHeight;
                    // we have to update room of object in the case it was changed
                    UpdateItemRoom(IndexTomb);
                    break;

Ok, now can build and play to verify if the cube is able to move over that gentle slope...

It works but there are problems...



The cube moves over the slope, ok, but from above picture we see that there are at least two problems:

  1. The cube keeps its common vertical orienting but while it is over a slope it is unrealistic. It should be inclinated to follow the rising shape of the floor

  2. Its Y coordinate is higher than point of the floor where it has its pivot (B point, at center of the cube). It seem that it touches only where we checked for freeway sector (A point)

The faster bug to fix is the number 2.
We had set as Y coordinate of the cube the value took from FLOOR structure but the (x,y,z) point checked to get that value it was the point in front of the cube to see if there was freeway, while we have to use the (x,y,z) position belove the pivot of the cube, so we'll change old code:

                    // in the case the cube was over a slope we have to change also its Y coordinate, setting it to same
                    // value of floor height
                    GET.pItem->CordY = FLOOR.FloorHeight;

We have to refresh the values in FLOOR structure calling newly the CheckFloor() function with the current coordinates of the cube:

                    // in the case the cube was over a slope we have to change also its Y coordinate, setting it to same
                    // value of floor height
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->CordY = FLOOR.FloorHeight;

Now we try newly our code in game...


Ok, the Y position bug it has been fixed now it remains the problem about vertical orienting.

How to align the Vertical Orienting with Slope



The vertical orienting is that you see in left side of above image.
When the cube moves over a rise we should change its OrientingV field, increasing its value to do point upper, with same angle of current slope.
To compute the degrees of this angle we should use trigonometric functions but there is a way to do more easier this computation.
Since the possible slope inclinations are a limited number (1, 2 or furtherly 3 clicks of inclination) we can discover once forever, these fixed angles and then to apply them to OrientingV field in according with the clicks of the slope.


In above image we find the data we need.
The number that we'll use is that for one click slope, so the value 0x0999

Now we have to add to the code in our progressive action the instructions to change the vertical orienting of the cube when it's moving over a slope
Where we changed the horizontal orienting:

                    // it's possible to do move the cube
                    // it the (relative) direction is different than 0 (0=continue forward), we'll have to change also
                    // the facin of the cube
                    if (Direction != 0) {
                              GET.pItem->OrientationH += Direction;
                    }

Now we add also the code to discover (newly) if the sector where the cube is moving it's a slope, we'll set new vertical orienting
while if it's a flat sector we'll set newly the 0x000 vertical orienting.

                    if (FLOOR.SlopeType == enumSLOPE.GENTLE_SLOPE) {
                              // change vertical orienting
                              GET.pItem->OrientationV = 0x0999;

                    }else {
                              GET.pItem->OrientationV = 0x0000;
                    }

And now try newly after above changes how it works in game...


It seems perfect but it seems... only.
Looking a moment first of above frame, we see another weirdness.
The cube turns immediatly its vertical orienting when the most of its volume is yet outside of next slope sector.


It seems an airplane in take-off phase.
It's not good.
It should be better if the turning changed gradually to reach the full inclination only when all cube is over the slope.

To find the value to add to vertical orienting to get the full change after a sector we have to do some computation.
We discover the number of frames required to cover the distance of one sector;

          FramesForSector = 1024 / Speed;

Now we have to divide the full turning we wish reach (0x0999) by the number of the frame:

          IncTurning = 0x0999 / FramesForSector;


Since the speed is costant, we could perform this compute when we are going to create the progressive action, inside of flipeffect 800 code, since it should be a waste of time compute this value everytime we move the cube when we can discover that at begin and then store it in some variable of progressive action.
For instance we could use the VetArgShort[0] since it is a short value.

This was the code for our flipeffect 800 in cbFlipEffectMine() function:

          case 800:
                    // 800:Experiments. Perform <&>experiment passing to it the (E)Value
                    Get(enumGET.PROGRESSIVE_ACTION,0,0);
                    GET.pAction->ActionType = AXN_MOVE_CUBE;
                    GET.pAction->Arg1 = ENDLESS_DURATE;
                    GET.pAction->Arg2 = 32;
                    GET.pAction->ItemIndex = 64;

                    break;

We'll declare a new local variable:

          short FramesForSector;

And the we add the code to discover the little increment to get a gradual turning:

                    FramesForSector = 1024 / 32;
                    GET.pAction->VetArgShort[0] = 0x0999 / FramesForSector;


While in progressive action we'll change the code from:

                    if (FLOOR.SlopeType == enumSLOPE.GENTLE_SLOPE) {
                              // change vertical orienting
                              GET.pItem->OrientationV = 0x0999;
                    }else {
                              GET.pItem->OrientationV = 0x0000;
                    }

to this new code:

                    if (FLOOR.SlopeType == enumSLOPE.GENTLE_SLOPE) {
                              // change vertical orienting
                              GET.pItem->OrientationV += pAction->VetArgShort[0];
                              if (GET.pItem->OrientationV > 0x0999) {
                                        // we cann't pass over the max inclination of 0x0999
                                        GET.pItem->OrientationV = 0x0999;
                              }

                    }else {
                              GET.pItem->OrientationV -= pAction->VetArgShort[0];
                              if (GET.pItem->OrientationV < 0) {
                                        // when we reached the flat orienting we have to stop the turning
                                        GET.pItem->OrientationV = 0;
                              }
                              
                    }

And now we build and try in game...


Ok, now it's very better.

Another bug to fix: rise or declivity?

There is another but to fix, but to discover it, we have to move lara in other wide room and wait from there the coming of the cube.


The cube explodes at end of flat path, while we are waiting the it moved down for the declivity.
We know that the cube has to explode when it doesn't find any free way in forward, left or right directions.
It's true that in above situation (see above picture) there is no free way at left or right, because the difference of height between Y coordinate of cube and right/left floor is too high.
But why cann't it simply goes on forward and move down for the slope?
The reason is in this code of the IsFreeWay() function:


                    // it is gentle slope, now we have to verify if the direction is the same of current direction (AbsDir) of our check
                    if (FLOOR.SlopeOrienting != AbsDir) {
                              // the slope has a different direction: no free way
                              return false;
                    }

The problem is that the cube is moving to north (it's not clear in above image but it is north that direction) while the slope in front of it has south orienting because the "rise" points to south.
So we have to change above code to accept the same direction of movement but also the opposite.
So we declare, in IsFreeWay() function, a new local variable to host the opposite direction:

          short OppositeDir;

And then we'll change above code in following way:

                    // it is gentle slope, now we have to verify if the direction is the same of current direction (AbsDir) of our check
                    OppositeDir = AbsDir - 32768;
                    if (FLOOR.SlopeOrienting != AbsDir &&
                              FLOOR.SlopeOrienting != OppositeDir) {
                              // the slope has a different current direction AND also the opposite direction of the object: no free way
                              return false;
                    }

The computation to get the OppositeDir "AbsDir - 32768", adds (or subtract) 180 degrees to current direction, getting the opposite value.
If it was "south" it will become "North", if it was "west" it will become "east" ect.

After above changes we'll try newly in game what happens...


But, WTF!?
No, WTF is not a mnemonic constant...
What's happened?
Now the cube move down for the declivity, it's good, but its vertical orienting it could be good only if it was a bike.

The problem is that we considered only the rise slope when we worked to change the vertical orienting of the cube.
Now the matter becomes more complicated, we have to plan better this stuff.

The situations that we have to manage are the following:

  1. Current sector is FLAT and next sector is a RISE

  2. Current sector is RISE and next sector is FLAT

  3. Current sector is FLAT and next sector is DECLIVITY

  4. Current sector is DECLIVITY and next sector is FLAT


To handle so many data we need of other variables of our progressive action, to keep values for:

We had already used the VetArgShort[0] to store the absolute increment to reach the wished vertical orienting, now we'll use also following variables:
The "VetArgShort[1]" to store the wished vertical orienting for next sector
The "VetArgShort[2]" to store the SIGNED increment to reach the wished vertical orienting.
The difference between values in "VetArgShort[0]" and "VetArgShort[2]" variable is that, while in "[0]" variable we have only the (always positive) quantity to add to change gradually the orienting, in the "[2]" variable we'll have the +/- value to add to reach that wished orienting.


Modify code in flipeffect 800

To avoid problems with residual values, we have to initialise to 0 the two new variables: "VetArgShort[1]" and "VetArgShort[2]"
So we'll add these two instrucions:

                    GET.pAction->VetArgShort[1] = 0; // wished final vertical orienting for next sector
                    GET.pAction->VetArgShort[2] = 0; // signed increment to reach final wished vertical orienting

And now the code for our flipeffect 800 it will become:

          case 800:
                    // 800:Experiments. Perform <&>experiment passing to it the (E)Value
                    Get(enumGET.PROGRESSIVE_ACTION,0,0);
                    GET.pAction->ActionType = AXN_MOVE_CUBE;
                    GET.pAction->Arg1 = ENDLESS_DURATE;
                    GET.pAction->Arg2 = 32;                              // speed
                    GET.pAction->ItemIndex = 64;
                    FramesForSector = 1024 / 32;
                    GET.pAction->VetArgShort[0] = 0x0999 / FramesForSector; // absolute increment for vertical turning
                    GET.pAction->VetArgShort[1] = 0; // wished final vertical orienting for next sector
                    GET.pAction->VetArgShort[2] = 0; // signed increment to reach final wished vertical orienting
                    break;



Modify code in progressive action

We should insert some other instructions after we verified to have found a free way for the cube.
In this situation we know that in FLOOR structure where was yet the data about next sector, that of free way, so we'll analys these data to discover the wished vertical orienting for next sector.
We have to rewrite from scratch the management of changing about vertical orienting, so we'll remove this code:

                    if (FLOOR.SlopeType == enumSLOPE.GENTLE_SLOPE) {
                              // change vertical orienting
                              GET.pItem->OrientationV += pAction->VetArgShort[0];
                              if (GET.pItem->OrientationV > 0x0999) {
                                        // we cann't pass over the max inclination of 0x0999
                                        GET.pItem->OrientationV = 0x0999;
                              }

                    }else {
                              GET.pItem->OrientationV -= pAction->VetArgShort[0];
                              if (GET.pItem->OrientationV < 0) {
                                        // when we reached the flat orientint we have to stop the turning
                                        GET.pItem->OrientationV = 0;
                              }
                              
                    }

And you'll replace it with this code to discover the values about next wished vertical orienting for next sector:

                    if (FLOOR.SlopeType == enumSLOPE.FLAT) {
                              // next sector is FLAT, set wished vertical orienting as 0x0000
                              pAction->VetArgShort[1] = 0;
                    }else {
                              // next sector is NOT flat, so it will be a slope.
                              // we discover if it is a rise or a declivity
                              // if the direction of the slope is the same of that of our object, then it is a "rise"
                              if (FLOOR.SlopeOrienting == GET.pItem->OrientationH) {
                                        // it is a rise
                                        // set wished value for rise
                                        pAction->VetArgShort[1] = 0x0999;
                              }else {
                                        // otherwise it will be a declivity
                                        pAction->VetArgShort[1] = -0x999;
                              }
                    }

Now we have to discover if we have to add or to subtract the increment to reach the wished value.
It depends by current sector where the cube is yet.
Anyway there is an easy trick to understand this: if current vertical orienting of cube is higher (as signed value) than next wished orienting, we'll have to reduce it, and so the increment will be negative. While if the current vertical orienting of the cube is lower than next orienting, we'll have to increase it to reach the final orienting.
So we'll type this other code:

                    // if the two vertical orienting, current and next, are the same, we have not to change anything
                    // and so we'll set as 0 the increment to change vertical orienting
                    if (GET.pItem->OrientationV == pAction->VetArgShort[1]) {
                              pAction->VetArgShort[2] = 0;
                    }

                    if (GET.pItem->OrientationV > pAction->VetArgShort[1]) {
                              // current vertical orienting is greaten than final: we have to use a negative increment to reach it
                              pAction->VetArgShort[2] = -pAction->VetArgShort[0];
                    }

                    if (GET.pItem->OrientationV < pAction->VetArgShort[1]) {
                              // current vertical orienting if less than final: we have to use a positive increment to reach it
                              pAction->VetArgShort[2] = pAction->VetArgShort[0];
                    }

And at end, we have only to update the vertical orienting of the cube, adding the signed increment to its value.

                    // change the vertical orienting
                    GET.pItem->OrientationV += pAction->VetArgShort[2];


The code of progressive action after last changes

After all these changes it's better show all code of our progressive action:

          case AXN_MOVE_CUBE:
                    IndexTomb= FromNgleIndexToTomb4Index(pAction->ItemIndex);
                    Get(enumGET.ITEM, IndexTomb, 0);
                    Direction = 0;
                    TestOk=false;
                    // try to continue to move forward
                    if (IsFreeWay(IndexTomb, Direction, pAction->Arg2) == true) {
                              // it is possible continue to move forward
                              TestOk=true;
                    }
                    if (TestOk==false) {
                              // it was not possible to move forward: try to turn at left
                              Direction= -16384; // (- 0x4000 90 degrees at west);
                              if (IsFreeWay(IndexTomb, Direction, pAction->Arg2) == true) {
                                        // it is possible turn at left
                                        TestOk=true;
                              }
                    }
                    if (TestOk==false) {
                              // it was not possible move forward and neither turning at left: try to turn at right
                              Direction = 16384;
                              if (IsFreeWay(IndexTomb, Direction, pAction->Arg2)==true) {
                                        // it is possible turn at right
                                        TestOk=true;
                              }
                    }
                    if (TestOk == false) {
                              // it has been NOT possible move in any direction: to do explode the cube
                              PerformActionTrigger(NULL, 14, IndexTomb, 2);
                              // and disable the progressive action for the cube
                              pAction->ActionType = AXN_FREE;
                              break;
                    }
                    // it's possible to do move the cube
                    // it the (relative) direction is different than 0 (0=continue forward), we'll have to change also
                    // the facin of the cube
                    if (Direction != 0) {
                              GET.pItem->OrientationH += Direction;
                    }
                    if (FLOOR.SlopeType == enumSLOPE.FLAT) {
                              // next sector is FLAT, set wished vertical orienting as 0x0000
                              pAction->VetArgShort[1] = 0;
                    }else {
                              // next sector is NOT flat, so it will be a slope.
                              // we discover if it is a rise or a declivity
                              // if the direction of the slope is the same of that of our object, then it is a "rise"
                              if (FLOOR.SlopeOrienting == GET.pItem->OrientationH) {
                                        // it is a rise
                                        // set wished value for rise
                                        pAction->VetArgShort[1] = 0x0999;
                              }else {
                                        // otherwise it will be a declivity
                                        pAction->VetArgShort[1] = -0x999;
                              }
                    }
                    // if the two vertical orienting, current and next, are the same, we have not to change anything
                    // and so we'll set as 0 the increment to change vertical orienting
                    if (GET.pItem->OrientationV == pAction->VetArgShort[1]) {
                              pAction->VetArgShort[2] = 0;
                    }
                    if (GET.pItem->OrientationV > pAction->VetArgShort[1]) {
                              // current vertical orienting is greaten than final: we have to use a negative increment to reach it
                              pAction->VetArgShort[2] = -pAction->VetArgShort[0];
                    }
                    if (GET.pItem->OrientationV < pAction->VetArgShort[1]) {
                              // current vertical orienting if les than final: we ahve to use a positive increment to reach it
                              pAction->VetArgShort[2] = pAction->VetArgShort[0];
                    }
                    // change the vertical orienting
                    GET.pItem->OrientationV += pAction->VetArgShort[2];
                    // now compute the new position of the item using its facing because we have already changed it if it was
                    // necessary
                    GetIncrements(GET.pItem->OrientationH, &IncX, &IncZ, pAction->Arg2);
                    GET.pItem->CordX += IncX;
                    GET.pItem->CordZ += IncZ;
                    // in the case the cube was over a slope we have to change also its Y coordinate, setting it to same
                    // value of floor height
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->CordY = FLOOR.FloorHeight;
                    // we have to update room of object in the case it was changed
                    UpdateItemRoom(IndexTomb);
                    break;


Now we try if it works...


Finally it works!

Last bug to fix

But our job has not yet been completed.


The cube, after having passed the declivity, continue to move until last wall, but instead by turning, it goes on, inside of the wall and then it explodes.
The reason is because there was a little door, in that position of the wall, and in spite it was too low to host the cube, it entered because we did checks only about the floor and never about the ceiling.
We have to verify also if the ceiling is enough high to allow the moving of the cube.

How to compute the size of a Moveable Item

To know if the top side of the cube is able to pass belove ceiling, we have to discover the Y coordinte of this top side.
In our case it should be easy, since we know that our cube is 1024x1024x1024 (it is a cube) game units.
But since we have to insert this control in our IsFreeWay() function, and we wish use it also with other moveables, we have to discover in run-time, the size of this generic object.
While we'll fix this problem, we could also fix that other limitation, about the fixed "511" value we used as half width of the object, in our control.
This was the code:

          Get(enumGET.ITEM, ItemIndex, 0);
          AbsDir = GET.pItem->OrientationH + Direction;

          GetIncrements(AbsDir, &IncX, &IncZ, Distance+511);

The last row is that to get the position of the point ad "Distance+511" in AbsDir direction.
We have to change also that "511" because other moveables, used with this function, could have another size.

Since also this target (discover the size of a moveable item) could be useful also in oter circustances, we'll create another little function.
We can name it: GetItemSize().
We'll have to pass to this function the index of moveable to analyse and we'll get two values for size: the top size Y coordinate, and the radius of horizontal volume, since it is this last value that it will be used to check the borders of the item.
Since there are two values to receive we could use the method to pass two empty boxes, two pointers to local variables, where the function will save the two returned values.
So our function (to declare upper of IsFreeWay() function, since we'll call it from this function) will have this code:

void GetItemSize(int ItemIndex, int *pTopY, int *pRadius)
{
          int CordYTop;
          int Distance;

          Get(enumGET.ITEM, ItemIndex,0);
          Get(enumGET.ITEM_COLL_BOX, ItemIndex,0);

To discover the size of the item we have to get its relative collision box.
We got also its item structure, of course.
We declared two local variabled to save temporarily the results.

          CordYTop = GET.pItem->CordY + GET.pCollItem->MinY;

The "MinY" field of relative collision box is the Y coordinate with lower value (negative, very often) but own for this reason it will be in upper position in 3d world, because in tomb raider game, moving up the Y coordinate will decrease.
Adding to the current Y coordinate of the item, its min (and upper) Y boundary of collision box, we got the Y top side value.

          Distance = GET.pCollItem->MaxX - GET.pCollItem->MinX;

          Distance = Distance / 2;

To discover the width of the item, in horizontal way, we compute the distance (difference) between its maxX vlaue and MinX value.
Warning that we get the correct result only in this way: "Max - Min", because if you did "Min - Max" you'll get a negative value.
Anyway the "MaxX-MinX" gives the full width, while we wish know the half width, so we divided by 2 this value.
Now just only fill the empty boxes (the pointers) "pTopY" and "pRadius" with our results:

          *pTopY = CordYTop;
          *pRadius = Distance;

}


The code of GetItemSize() function

The final code of our function will be this:

void GetItemSize(int ItemIndex, int *pTopY, int *pRadius)
{
          int CordYTop;
          int Distance;

          Get(enumGET.ITEM, ItemIndex,0);
          Get(enumGET.ITEM_COLL_BOX, ItemIndex,0);

          CordYTop = GET.pItem->CordY + GET.pCollItem->MinY;

          Distance = GET.pCollItem->MaxX - GET.pCollItem->MinX;
          Distance = Distance / 2;

          *pTopY = CordYTop;
          *pRadius = Distance;
}


Change to the IsFreeWay() function to work with any moveable

Now we modify the code in IsFreeWay() function to do a check also ceiling of new direction, and to use a generich half size of the item instead by using the constant "511"
We declare in IsFreeWay() function the two variables for HalfWidth (or radius) and for TopSideY:

          int HalfWidth;
          int TopSideY;

Then we call our new function GetItemSize() to discover the TopSideY and HalfWifth values for our moveable:

          GetItemSize(ItemIndex, &TopSideY, &HalfWidth);
          GetIncrements(AbsDir, &IncX, &IncZ, Distance+HalfWidth);

We inserted its call before to call GetIncrements() since we need of HalfWidth value (to replace the old "511")
And we changed the "Distance+511" with "Distance+HalfWidth"
Now, we can type the control on the ceiling heigth, after we called CheckFloor() function, because we need of the value of the ceiling in the next sector we are studying.

          CheckFloor(NewX, NewY, NewZ, GET.pItem->Room);

          if (FLOOR.TestFullWall == true) {
                    // cube stopped by a wall
                    return false;
          }

          if (TopSideY < FLOOR.CeilingHeight) {
                    // the object touch the ceiling: no free way
                    return false;
          }

We set the condition:

          if (TopSideY < FLOOR.CeilingHeight) {
                    // the object touch the ceiling: no free way
                    return false;
          }

because when alfa is less than beta its means that alfa is in a upper position, in 3d world, of beta, and in our case if the TopY border of the object is upper that (lower) Y coordinate of ceiling, this means that there is no space to move there.
Now we build the project and try if the cube moves correctly for all its path


Final result of our Robotic Cube



Goal.
The cube turns when it find too lower ceiling, it turns when there is a hole or when it finds a steep slope and at end it moves until opposite wall, where it will explode.

Homeworks

If you wish try to improve the robotic cube yourself, here you find a track for your homeworks

Homework 1
When the cube meets an obstacle it turns its facing of 90 degrees (at left or right).
We see this in game, looking its blue face, that is that where it has its current facing.

The immediate turning is not so fine to see.

Your homework will be to turn gradually the cube before moving it for new direction.

To realise this target you should work in this way:

  1. You have to use another variable of progressive action to store the wished HORIZONTAL orienting, like we did for wished vertical orienting

  2. You need of another variable, of progressive action, to remember if the cube is in "move forward" phase, or in "turning on itself" phase.
    This value will work like a state-id

  3. When this "state-id" is "1", it means that, in progressive action code, you'll have only to change a bit the current facing of the cube, omitting controls about next sector because the cube is not really moving in the space but only turning on its pivot

  4. To store the increment to add to H orient, positive or negative, you'll need of another variable, to store this signed increment

  5. When the cube is in turning mode (state-id =1) you'll have to check if the turning has been completed and in that case you'll set to 0 the state-id value, to do move newly the cube from next frame.


Homework 2
This second homework is easier...

We used our flipeffect for experiment (F800) to move our cube with fixed ngle index = 64.
It was an experiment, ok, but if we wish using really this skill to move an item with robotic movements, we should be able to use also other moveable indices.
So the homework in this case is to create a new Action trigger and move the code from F800 to new action trigger.
Since the action trigger have also an Index field, we can use the index set in action trigger to discover the item to move.
In this way that action trigger will be able to manage any moveable item.
Note: remember that the index of moveable that you find in action trigger parameter, it is already a tomb raider index, so you have not to convert it from ngle to tomb format.




Exercise 3: The Cleaner Robot

This exercise begins in room 11 of plugins project


In this exercise we'll apply many discoveries of previous chapter about robotic movement, using an object with a real look of robot, and giving to it all features of a standard tomb raider object.
The main target of this chapter it will be own to understand how to create a new object that it will be triggered in game with a common "trigger" trigger, that it will be able to hurt or kill lara and it will have its AI skills.

We'll use as object that cleaner robot of Tomb Raider 3 (picture at left).
In Tomb Raider, the "standard" objects ( but perhaps it should be better saying "enemies") have a particular management.
They have their slot in wad file (our robot has the slot named "ROBOT_CLEANER") and they have an AI behavior.

We have known very often enemies, while we created custom levels or changing their animations, but looking these enemies from a point of view of the code that handles them, we'll discover new stuff.
In slot structure there are infos about meshes and animations of the given moveable but when that slot it will be loaded in tomb raider game, other data will be added to slot structure in dynamic way.


Note: the object it has been a bit changed from original tr3 version because that object was able to move only on particular geometry of rooms. It was a bit higher than floor and the cables were too long to be hosted in 1x1 tunnels. Also its width was to large to fit in one sector tunnel.
To be able to use it in most common geometry of tunnels with one sector of width I had to restyle it: moving down its meshes to touch the floor, stretching its width to fit in one sector and I had gotten shorter also the electric cables.


The Slot structure

Here we have the declaration of StrSlot structure:

typedef struct StrSlot {
          WORD TotMesh;                    // 0
          WORD IndexFirstMesh;          // 2
          int IndexFirstTree;          // 4
          int IndexFirstFrame; // 8
          void *pProcInitialise;          // 0C
          void *pProcControl;                    // 10          
          void *pProcFloor;                    // 14
          void *pProcCeiling;                    // 18
          void *pProcDraw; // 1C
          void *pProcCollision; // 20
          WORD DistanceForMIP; // 24
          WORD IndexFirstAnim; // 26
          short Vitality;                              // 28
          WORD DistanceDetectLara;                    // 2A
          WORD ss_Unknown3;                    // 2C
          WORD FootStep;                              // 2E
          WORD TestGuard;                    // 30
          WORD Flags;                                        // 32 (FSLOT_ flags)
          void *pProcDrawExtras;          // 34
          int ShatterableMeshes;                    // 38
          int ss_Unknown5;                    // 3C
}SlotFields;

The slot structure is the main "group" of data that you find in .wad files, where all objects are stored before linking them into the tr4 file.
We see in above declaration many variables used to set common feature of that object: about meshes, animations and its internal data, frame and tree data.

About the variables that begin with "pProc..." text, they are empty (0 value) in the wad file, but when that object it will be loaded, the tomb raider engine will initialise them setting the address of functions (or "procedures" like that name remembers: "pProc" = "Pointer to Procedure")

So, it happens that tomb engine will handle any object calling the procedures (or functions) whom address got from the above "pProc" variables.
When we wish add a new object, that it was missing in the past, we have to intialise those variables because tomb raider engine will not do, since that old program cann't know these new objects.
Main target of this exercise it will be to insert our new object (the cleaner robot), in those managed by tomb engine in automatic way.
In this way we'll learn also how the enemies work in detail and these informations could be useful also when we wished only to change some already existing enemies, changing their behaviors and skills


The management Procedures of moveable Items

If we wish supply a management procedure for a new item, we'll set the address of some our function in above "pProc" fields of slot structure.
In this way, when tomb engine will manage our object, it will call our functions.
For above reason, we have to know the parameters and the target of these functions to be able to create a code that worked correctly with tomb engine requirements.


The Control() procedure
In the variables named "pProcControl", we'll place the address of the Control() procedure for that object.
The Control procedure has this declaration:

void ControlObject(short ItemIndex);

We need to know the declaration because we have to set the address of a function that had same input argument, and further returned type values.
In the case of Control() procedure, tomb4 will pass to this function a single input argument: the ItemIndex.
ItemIndex is the index of moveable item with the given slot.,
For instance, our ROBOT_CLEANER is only one slot, but then in game there could be many robot cleaner items.
The function name will be the same but to know what is the item we are hanndling, we'll use the ItemIndex.
The Control() procedure is the main procedure, it controls most of skills and features of the item: it will move the item in the level, it will check to avoid collision and it is always this function to manage the AI skills of the item.
For instance, when we'll use our code about Robotic Random Movemement of previous exercise, we'll have to move amost all that code, from progressive action, to our Control() procedure.


The Initialise() procedure
The pProcInitialise variable of slot structure host the address of Initialise() procedure.
The Initialise() procedure has same declaration of Control() procedure:

void InitialiseObject(short ItemIndex);

This function will be called only once, when the level is going to be loaded.
The target of this function is to fill the StrItemTr4 structure of the given item.
Tomb4 engine will allocate a moveable structure for each object of that kind, but this structure is almost empty, we have to initialise it.
The most common operations we'll do in Initialise() procedure, will be to set current first animation, state-id, next state id and current frame.

Example:

void InitialiseRobotCleaner(short ItemIndex)
{
          Get(enumGET.Item, ItemIndex,0);
          GET.pItem->AnimationNow = 0;
          GET.pItem->StateIdCurrent = 2;
          GET.pItem->StateIdNext= 2;
          GET.pItem->FrameNow = CurrentFrame;
}

Above it's only an example, of course.
Some items will requires also other settings, and then we'll have to explain better how to compute the CurrentFrame value to initialise "FrameNow" variable of item structure.


The Collision() procedure
The pProcCollision variable of slot structure host the address of Collision() procedure.
This is the declaration of CollisionObject() procedure:

void CollisionObject(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision);

The name of this procedure could get confusion.
For "collision" we don't mean collisions with walls, ceiling, or other objects but only collisions with Lara.
Pratically we'll type in this procedure the code to manage the situation when lara is very closed to this item.
What will it happen?
The item hurts lara, or the item is a vehicle and lara will get in the vehicle? Or is it a door and lara will open it with a kick?

It depends by what is the item of course.

About the arguments, the ItemIndex is the index of the item for given slot (in our case, it will be the index of some our robot cleaner), the pLara is a pointer to the structure item of Lara object, while the pLaraCollision ... it's complicated to explain.
In spite I studied for long time this structure I've not found a good way to use it.
Pratically it is a huge structure with many data about walls and ceiling of sectors closed to lara.
This structure should be a way to have in advance many collisional data we could use to detect the collisions between lara and room geometry.
Anyway I've not discovered all fields of that structure.
However, that parameter is important, because other procedures (to compute collision between objects) will require that structure, so we have its pointer and sometimes we'll pass it to some other tomb4 functions.


The Floor() and Ceiling() procedures
The pProcFloor and pProcCeiling variables of slot structure host the addresses of FloorObject() and CeilingObject() procedures.
The FloorObject() and CeilingObject() declaration have the same declarations:

void FloorObject(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision);
void CeilingObject(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision);

They work in similar way of CollisionObject() (and they have also same declaration) but in this case they will be called only when Lara is over (or belove, for the Ceiling) the item.
Most items have no Floor() or Ceiling() procedures, because only items where Lara is able to walk over them or to interact from belove with ceiling, will have these procedures, like TrapDoor, twoBlockPlatform, TrapDoorCeiling ect.


The Draw() procedure
The pProcDraw variable of slot structure host the address of DrawObject() procedure.
The DrawObject() procedure has this declaration:

void DrawObject(StrItemTr4 *pItem);

How you can see in this case the input argument is not the index of the item but it is directly its item structure.
This is a very particular procedure. Because in spite it has the important function to draw the object, it is not necessary you type a value in pProcDraw pointer, because all slots, also new slots, will be already initialised with a default drawprocedure managed by tomb 4 engine.
Pratically it is the same procedure used to draw all animating items.
The only change you could do is to type NULL in this field in the case your new object was a null-mesh item, or, to replace the default draw procedure with yours in the case you object has not traditional look of object with mesh, but it is show some volumetric or particlers effect like smoke, flames, lights.
The default draw procedure indeed, works fine when it has to show the meshes of the object following position of current animation.


The DrawExtras() procedure
The pProcDrawExtras variable of slot structure host the address of DrawExtrasObject() procedure.
The declaration of DrawExtrasObjects() is the same of above DrawObject() procedure:

void DrawExtrasObject(StrItemTr4 *pItem);

Also this procedure is to draw the object but it is a second draw procedure to use when your object has both tradition mesh shape and either volumetric/particle nature.
In this situation, you'll let unchecked the previous DrawObject() procedure, to do show the meshes of your object like any other animating or enemy items, but then you'll set in pProcDrawExtras field, the pointer of your procedure to draw the extra feature (not meshes) of your object.
For instance in planet effect the drawextra proceure it has been used to show the lightnings that linked different globes at end of the planet effect.
In jeep and motorbike the DrawExtra() will show the exhaust smoke (particles) of the veihcle or the speedometer (sprite and 2d graphic) ect.


How to change Management Procedure of Slot

When we created a new object, we'll have to do the code for basic management procedures: Initialise(), Control() and Collision() procedures, using same declaration we saw in previous chapter, and then set the address of above functions in "pProc..." fields of the slot structure used to store our new item.
About "how to do" this (or better "when" or "where" to do this) we have to set these management procedures when the objects have been just loaded (a moment after) but also a moment before these management procedures will be called from tomb4 engine, because the Initialise() procedure, for instance, it will be called very early.
We'll place the code to set our management procedure in "pProc" fields, when the cbInitObjects() callback will be called.


The cbInitObjects() callback
The cbInitObjects() callback function will be called own in that short right moment we described above.
To get this callback (it's not a default callback so we have to require it) we'll type in the code of RequireMyCallBacks() function, this call:

          GET_CALLBACK(CB_INIT_OBJECTS, 0, 0, cbInitObjects);

Then we have to create really the cbInitObjects() function, typing it in upper position respect RequireMyCallBacks() function:

void cbInitObjects(void)
{

}



What does it happen when we use an Unmanaged Enemy?

Now we are going to add code to animate our ROBOT_CLEANER, but first beginning it's interesting to verify what happens when we place an unmanaged moveable in the level.
Play the game and select level "Exercise 3: The Cleaner Robot"
And move Lara to reach where is our robot


How we can see there is a problem.
Try to move Lara towards the robot, she will enter inside, acrossing the object (A Picture)
Also trying to trigger it moving lara over the blue sector (B picture) we'll have no change.

Above problem will disappear only when we'll add the management procedures to handle the robot.


Initialise the Cleaner Robot

After we have required a callback for the moment that trng has just loaded the objects, usign following code, typed in RequireMyCallbacks() function:

          GET_CALLBACK(CB_INIT_OBJECTS, 0, 0, cbInitObjects);

We'll create our callback function that it will be called when trng loaded the objects:

void cbInitObjects(void)
{

}

We'll get the slot structure of ROBOT_CLEANER and then we have to verify if the ROBOT_CLEANER object exist really in current level, otherwise we are working on an empty slot:

          Get(enumGET.SLOT, enumSLOT.ROBOT_CLEANER, 0);
          if ((GET.pSlot->Flags & enumFSLOT.PRESENT) == 0) {
                    // the flag PRESENT is missing, so there is NO robot cleaner in this level: we quit immediatly the code
                    return;
          }

In above code we got the StrSlot structure of the robot cleaner item, and we tested if the flag FSLOT_PRESENT is enabled in Flags field of this structure.


How to check if a flag is missing
The condition we used is a news and we have to explain it better:

          if ((GET.pSlot->Flags & enumFSLOT.PRESENT) == 0) {

We have already seen that to test the presence of a single flag (or bit) in some variable we have to use the "&" (binary and) operator.
But in above instruction we had to discover if it is missing.
The condition in C++ language (and other programmin languages) work in this way: if after a condition, or also a mathematical operation, the result is 0, then the result is false, while when the result is a any value different than 0, the condition is true.
For instance we could type this weird code:

          int Alfa;

          Alfa = 1423;

          if (Alfa) {
                    // alfa is different that zero
          }

In above example there was no real comparison, anyway the "if ()" will result "true" because the result in round parenthesis is different than zero.
For this reason when we wish discover if a flag (a single bit, i.e. a power by 2 value: 1,2,4,8,16,32,64,128,256 ect) is present or less we perform an binary and, using the "&" operator, and the result will be: if that value is present, the result will be own that value, while if it is absent, it will be 0.
Some examples:

          int Alfa;
          int Result;

          Alfa = 4;
          Result = Alfa & 4;
          // now Result variable will contain "4"
          Result = Alfa & 8;
          // now Result variable contains "0"

So, coming back to our condition:

          if ((GET.pSlot->Flags & enumFSLOT.PRESENT) == 0) {


We got a value inside of inner round parenthsis "(GET.pSlot->Flags & enumFSLOT.PRESENT)", in the case the value of "enumFSLOT.PRESENT" ("1" in this case), is present in Flags variable, the result will be "1", while if it is missing it will be "0"
So we compared the result of that operation with "== 0)" to have as condition: that flag is missing.

Now we begin to fill the slot structure with our management procedure.
We could begin with the Initialise() procedure
So we have to create a InitialiseProcedure() using same declare we saw in previous paragraph:

void InitialiseObject(short ItemIndex);

But we'll use a name to remember that it owns to robot cleaner, so we'll type (upper in the source respect the cbInitObjects() function, since we'll use its address from there):

void InitialiseRobotCleaner(short ItemIndex)
{

}

Now in cbInitObjects() callback we'll set in pProcIntialise field the address of our InitialiseRobotCleaner() function:

          GET.pSlot->pProcInitialise = InitialiseRobotCleaner;



The Pointer to function
Why have we not used the "&" (unary operator) to get the address of the function, like we do for variables (&Alfa)?
Because the function name is like a vector name: it is already a pointer, so when we use it, omitting the round parenthesis, we are managing a pointer to that function.

We have also to set some flags, to inform tomb engine abouth what kind of object is this.
In "Tomb_nextgeneration.h" source, you can see the full list of (known) flags for slot:

// mnemonic constants for flags of StrSlot structure
#define FSLOT_NONE 0x0000 // used only to clear Flags field, to test presence of slot use (Flags & FSLOT_PRESENT)
#define FSLOT_PRESENT 0x0001 // object of this slot is present
#define FSLOT_AI_STANDARD 0x0002 // (not sure) enemy with traditional AI and movements (yes baddies, no wraith)
#define FSLOT_CHANGE_POS_ITEM 0x0008 // this item could change its position: it's necessary save its coordinate in savegame
#define FSLOT_MOVED_BY_ANIMATIONS 0x0010 // (not sure) moved whereby animations
#define FSLOT_SAVE_ALL_DATA 0x0020 // this item requires many data, other position, to be saved in savegame
#define FSLOT_AFFECT_LARA_AT_CONTACT 0x0040 // (not sure) lara could interacts with this item when she is closed to it
#define FSLOT_SFX_LOCAL_SOUND 0x0100 // the sounds of this moveable played in its local position (otherwise: global sound)
#define FSLOT_USE_COLLISION_BOX 0x0200 // item collisions will be checked whereby its collisional box
#define FSLOT_AMPHIBIOUS_CREATURE 0x0400 // creature is able to move underwater
#define FSLOT_HIT_BUT_NOT_HURT_BY_SHOTGUN 0x0800 // (not sure) shotgun ammo disturb them without hurt them (mummy and skeleton)
#define FSLOT_NO_DAMAGE_FOR_NO_EXPLOSIVE_AMMO 0x1000 // invulnerable at not explosive ammo
#define FSLOT_SAVE_MESH_MASK 0x2000 // this item could change visibility of its meshes: save also mesh visibility status


Looking above list, probably the flags in according with our robot will be following:

          GET.pSlot->Flags = enumFSLOT.AFFECT_LARA_AT_CONTACT |
                                                            enumFSLOT.CHANGE_POS_ITEM |
                                                            enumFSLOT.NO_DAMAGE_FOR_NO_EXPLOSIVE_AMMO |
                                                            enumFSLOT.SFX_LOCAL_SOUND |
                                                            enumFSLOT.USE_COLLISION_BOX;

Note: we used "|" (binary or operator) insteady by the "+". Really in above code it will work fine also "+" operator, anyway since it's more correct the "|" it's better you take to habit to work correctly from the start.

About chosen flags, they will inform tomb engine that our object:

  1. It could damage lara at contact (AFFECT_LARA_AT_CONTACT), so tomb engine should call our (to set, yet) Collision() management procedure, to get the chance to manage this situation


  2. This object could move in the space (CHANGE_POS_ITEM) , so it's necessary save to the savegame its coordinates (and then, restore them)

  3. It's not possible hurt it with common ammo (NO_DAMAGE_FOR_NO_EXPLOSIVE_AMMO) , and probably neither with explosive ammo, but this setting we have to set in another code

  4. The sounds of this object should be located in 3d space (SFX_LOCAL_SOUND) , where there is the object. (most common setting)

  5. To detect the shape of this object, about collisions, it will be used its collision box (USE_COLLISION_BOX), got from animation data

          

How to initialise the item structure of our Cleaner Robot

In above paraghrap, we set some value in Slot structure of our robot, and also our InitialiseRobotCleaner() function.
Now, the tomb engine will call our InitialiseRobotCleaner() function, when it is initialising all objects.
So we have to type some code in InitialiseRobotCleaner() function, to initialise the StrItemTr4 structure of any cleaner robot, present in current level.
The main target of this initialisation is to set first animation, state-id and frame for the item.
Looking in Animation Editor the robot cleaner, we discover that is really a very easy and plane object.
It has only one animation and one state id, both with values=0, of course.
So we'll set these values:

void InitialiseRobotCleaner(short ItemIndex)
{
          Get(enumGET.ITEM, ItemIndex,0);

          GET.pItem->StateIdCurrent = 0;
          GET.pItem->StateIdNext = 0;
}

We had the need also to get the effective structure of the item, since we got only its index as input parameter.
How you see, setting the state id it's easy, we type simply the wished values.
But for the animation number it's a bit more complicated, because the value of animation in the structure item (AnimationNow, field), it's not a relative animation index (where the first animation of that object it will be "0", and the second it will be "1") but it is an absolute value, regarding all animations (of any moveable) present in wad/tr4 file.
So if the animations of ROBOT_CLEANER begins from index = 876, of the total list, this means that its (realtive) first animation (0 as relative index), it will be "876", but if we wish set the animation number "3" of our object, we should type 876+3= 879 as AnimationNow index in Structure item.
To discover what is the absolute animation index to type in item structure we have to know what is the first absolute index for robot animations.
We have this value in StrSlot structure of given item.
So we'll have to require (with Get() function) also the slot structure of that item to discover that value:

          Get(enumGET.SLOT, enumSLOT.ROBOT_CLEANER,0);


Now we got strslot structure of the robot, and inside of this structure we find the index of first animation.
So we can use it to compute the absolute index for first animation of the robot, the animation = 0.
In this case it will be own the same value of first animation, but you should imaginate a "IndexFirstAnim+0" to set the wished animation number:

          GET.pItem->AnimationNow = GET.pSlot->IndexFirstAnim;


Now we have to set the current frame for this animation, and the operaton is alike of above but not the same.
In this case we have to read the first frame of the animation we had set (using the absolute index of animation).

So, we'll have to get the the structure of animation, using the absolute animation index we discovered.


          Get(enumGET.ANIMATION, GET.pItem->AnimationNow,0);

Now we read from animation structure the FramStart value and copy it to FrameNow of item structure:

          GET.pItem->FrameNow = GET.pAnimation->FrameStart;



How to set the FITEM_ flags for FlagsMain field of structure item
In previous paragraphs we set stateid, animation and frames, now we have to set also the flags of structure items.
These are other, different, flags respect to those for slot structure.
While the FSLOT, for slot structure work for that kind of object, and it will be same for all objects of that kind, the flags (FITEM_) for structure item will be different for each item, or at least, they could change in the progress of the game, for instance because an enemy could be killed while another is yet living ect.
The available FITEM_ flags are the following:

// mnemonic constant for FlagsMain of StrItemTr4 structure
#define FITEM_NONE 0x0000 // used only to clear flags
#define FITEM_ACTIVE 0x0001 // item is active (it will move) flag set after calling AddActiveItem()
#define FITEM_CREATURE 0x0002 // Creatures are invisible until they have not been triggered
#define FITEM_NOT_VISIBLE 0x0004 // the item was not visible at start
#define FITEM_GRAVITY_AFFECTED 0x0008 // the item is falling down (or jumping up) and it's subject to gravity simulation, currently
#define FITEM_FLAG_10 // (misterious) it could be: enmey has already a defined target, or, enemy has been hurt, or enemy has been killed
#define FITEM_NOT_YET_ENABLED 0x0020 // the item has not yet been enabled (triggered)
#define FITEM_KILLED_WITH_EXPLOSION 0x0040 // trng flag, added to remember that this enemy has been killed with an explosion
#define FITEM_POISONED 0x0100 // enemy (or lara?) has been poisoned
#define FITEM_AI_GUARD 0x0200 // enemy was over a AI_GUARD item
#define FITEM_AI_AMBUSH 0x0400 // emeny was over a AI_AMBUSH item
#define FITEM_AI_PATROL1 0x0800 // enemy was over a AI_PATROL1 (and perhaps AI_PATROL2)
#define FITEM_AI_MODIFY 0x1000 // enemy was over a AI_MODIFY item
#define FITEM_AI_FOLLOW 0x2000 // enemy was over a AI_FOLLOW item
#define FITEM_THROWN_AMMO 0x4000 // (not sure) it used with visible ammo like greande or arrows of crossbow


Looking above list, we can choose the flags appropriate for our robot:

          GET.pItem->FlagsMain = enumFITEM.NOT_YET_ENABLED | enumFITEM.CREATURE | enumFITEM.NOT_VISIBLE;

The flag "CREATURE" could seem weird for a robot and indeed in other situation we could omit it for robots, but the meaning of CREATURE flag is that before triggering the "creature" will be invisible.
So we chose to get not visible the object until its triggering in game, like it happens for baddies.
It was possible also omitting that flag, of course.
Now if we build the plugin and test it in game, we see that the robot is not visible but it becomes visible if we trigger it moving lara on blue sector.

This is common behavior of many enmeies.

Anyway lara is yet able to move inside of it, because we have not yet set the collision procedure.


How to set the Collision Management Procedure

The CollisionObject() procedure will be called everytime the item is enough closed to Lara, anyway the distance could be very far, about six sectors, 1024 * 6 game units.
Our code should ignore the problem when the Item has not yet been enabled (if it is invisible) because there will be no collision if it's missing in game.
We should skip the collision code if lara is too far to be touched from item, and we can discover the distance using GetMaxDistance() procedure.
Then, when all above conditions have not to quit the procedure, we'll perform a check if bound collision box of lara is touching the item.
So we have to create our collision function for robot cleaner:

void CollisionRobotCleaner(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision)
{
          Get(enumGET.ITEM, ItemIndex, 0);

}

First step it will be, as usual, to get the item structure of our robot.


TriggerActive() tomb4 function
Now we check if it it has been triggered, otherwise we'll quit immediatly.

          if (TriggerActive(GET.pItem)== false) return;

The TriggerActive() function, is a tomb4 procedure to verify if current item has been triggered in game.
So, if our robot has not been triggered we can skip the collision code since there will be no collision.

Note: probably above call in our Collision() procedure is futile, since if the object has not yet been triggered, the collision procedure should be neither called, anway it depends by the flags we set in FlagsMain of item structure, so we'll do anyway this call, because with other moveables it could be necessary and it's better remember to check if the item has been triggered or less, before affecting it.


GetMaxDistance() function
Now we verify if lara is yet too far to be touched by the robot:

          if (GetMaxDistance(&GET.pItem->CordX, &pLara->CordX, false) > 1024) return;

Since the distance between Lara and the robot has to be less that one sector to have a contact, we can get faster our code, saving time of futile controls, if we discover that lara is more far than one sector.
With above code, we gave to GetMaxDistance() function, the coordinate (x,y,z) of robot and lara, passing only the address of first coordinate (CordX) then the function will read others coordinates CordY and CordZ whereby the address pointer.


TestBoundCollide() tomb4 function
Now, we discover if the two bounding (collision) box, of lara and robot, are overlapped:

          if (TestBoundCollide(GET.pItem, pLara, pLaraCollision->LaraSizeX)==false) return;

Also TestBoundCollide() is a tomb4 function.
It requires two pointer of structure of moveable items (StrItemTr4 *) as first two arguments, and then the distance argument.
From above code we see to have used a variable of misterious pLaraCollision structure. Probably LaraSizeX is the radius of lara in horizontal view, like we had computed in Exercise 2 using the GetItemSize() function.
If TestBoundCollide() returns "false" there is no collision and we can quit the code.


TestCollision() tomb4 function
While if return is "true" is not yet sure that there is a collision. We have to do a more precise computation using the TestCollision() function:

          if (TestCollision(GET.pItem, pLara)==false) return;

At end of all above conditions, we discovered that Lara and robot are in collision (if, first, the code didn't quit with above conditions...)
Now we have to choose what to do
We can simply avoid that lara enters in the robot, pushing her aways.
Or we could hurt or kill her.


ItemPushLara() tomb4 function
As first experiment we keep only push far away her, using the ItemPushLara() tomb4 function:

          // lara and robot are in collision
          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);

The ItemPushLara() is another tomb4 function.
You can see the declaration of its point in "DefTomb4Funct.h" source file, like for other tomb4 functions:

typedef void (__cdecl *TYPE_ItemPushLara) (StrItemTr4 *pItem, StrItemTr4 *pLara, StrCollisionLara *pCollLara, bool TestChangeLaraAnim, int Parameter);

Ignoring the weirdnesses to declare a pointer to an external function (that stuff about "__cdecl * TYPE_..") we can see the arguments required to this function:

StrItemTr4 *pItem argument
--------------------------------------
If the item structure of first moveble, the item different than Lara

StrItemTr4 *pLara argument
---------------------------------------
Is the second item strucutre of moveable, usually it should be Lara

StrCollisionLara *pCollLara argument
--------------------------------------------------
This is the pointer of the (misterious) structure about collisions of Lara. We got that pointer as argument of our CollisionRobotCleaner() function, and now we'll pass this pointer to ItemPushLara() function.

bool TestChangeLaraAnim argument
-------------------------------------------------
It will have "true" or "false" like value to pass, and it is to choose if change the lara animation, to contract its body while she will puhsed aways ("true"), or less ("false")

int Parameter argument
-------------------------------
I'm not sure about this argument. It could be to choose what of two items move really, changing its position, "1" means the first item, "2" the second.
But I'm not sure. Anyway tomb4 code used amost always "1" and so we'll use "1", too.

Now we have to link our Collision procedure with management procedures of tomb4, otherwise it will remain unplugged.
So we go in cbInitObjects() callback, and set the address of our Collision procedure:

          GET.pSlot->pProcInitialise = InitialiseRobotCleaner;
          GET.pSlot->pProcCollision = CollisionRobotCleaner;


It's better summarize the code we typed for our robot.

Summary of current Robot Cleaner Code

In callback function, to initialise slot objects, we initialised the slot structure of our robot, setting also two management procedures: the Initialise() and the Collision() procedures:

void cbInitObjects(void)
{
          Get(enumGET.SLOT, enumSLOT.ROBOT_CLEANER, 0);
          if ((GET.pSlot->Flags & enumFSLOT.PRESENT) == 0) {
                    // the flag PRESENT is missing, so there is NO robot cleaner in this level: we quit immediatly the code
                    return;
          }

          GET.pSlot->pProcInitialise = InitialiseRobotCleaner;
          GET.pSlot->pProcCollision = CollisionRobotCleaner;

          GET.pSlot->Flags = enumFSLOT.AFFECT_LARA_AT_CONTACT |
                                                            enumFSLOT.CHANGE_POS_ITEM |
                                                            enumFSLOT.NO_DAMAGE_FOR_NO_EXPLOSIVE_AMMO |
                                                            enumFSLOT.SAVE_ALL_DATA |
                                                            enumFSLOT.SFX_LOCAL_SOUND |
                                                            enumFSLOT.USE_COLLISION_BOX;
}


The Initialise() procedure set only animation, state id and frame for any moveable robot item:

void InitialiseRobotCleaner(short ItemIndex)
{
          Get(enumGET.ITEM, ItemIndex,0);
          GET.pItem->StateIdCurrent = 0;
          GET.pItem->StateIdNext = 0;
          Get(enumGET.SLOT, enumSLOT.ROBOT_CLEANER,0);
          GET.pItem->AnimationNow = GET.pSlot->IndexFirstAnim;
          Get(enumGET.ANIMATION, GET.pItem->AnimationNow,0);
          GET.pItem->FrameNow = GET.pAnimation->FrameStart;
          GET.pItem->FlagsMain = enumFITEM.NOT_YET_ENABLED | enumFITEM.CREATURE | enumFITEM.NOT_VISIBLE;
}


While as collision procedure we detect if there is a collision and it there is, we'll push aways Lara:

void CollisionRobotCleaner(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision)
{
          Get(enumGET.ITEM, ItemIndex, 0);

          if (TriggerActive(GET.pItem)== false) return;

          if (GetMaxDistance(&GET.pItem->CordX, &pLara->CordX, false) > 1024) return;

          if (TestBoundCollide(GET.pItem, pLara, pLaraCollision->LaraSizeX)==false) return;

          if (TestCollision(GET.pItem, pLara)==false) return;

          // lara and robot are in collision
          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);


}

Now we can build the plugin and try in game what happens...

We stopped lara, finally



Lara tries to move towards the robot but she is not able to go on.
She has yet the running animation because we set as TestChangeLaraAnim argument, "false", to avoid to change her animation with other hardcoded animation.
Anyway, as experiment, we can change temporarily that value to "true" to see what happens in game...

          ItemPushLara(GET.pItem, pLara, pLaraCollision, true, 1);

Now try in game....

That weird animation for Lara collision



I don't like so much that animation, anyway you choose as you wish.


The Control() procedure for our Robot Cleaner

Now the collision works fine but our robot is frozen and neither the animation to turn the cables is working.
The reason is because it's missing the Control() management procedure.
After trigger activaction the robot will be drawn, because the (default) Draw() procedure is present but now we have to give to it a control procedure.

void ControlRobotCleaner(short ItemIndex)
{
          Get(enumGET.ITEM, ItemIndex,0);

          if (TriggerActive(GET.pItem)==false) return;

}

Above code is the most common start.
We get the item structure and then we verify if the robot has been triggered.
If it hasn't, we can quit immediatly.

AnimateItem() tomb4 function

Now to have the current animation 0 (that we had set in InitialiseRobotCleaner()) working, we call the AnimateItem() function.
This is another tomb4 function and its main target is to perform the current animation, increasing the frame, applying speed and acceleration, performs the jump to next animation or next state id.
This function controls also the gravity simulation if it has been enabled by correct flag (FITEM_GRAVITY_AFFECTED) in FlagsMain of item structure.

          AnimateItem(GET.pItem);

The AnimateItem() function, requires the item structure of moveable.

Now we set our ControlRobotCleaner() function in slot structure, adding it to the code of cbInitObjects() callback:

          GET.pSlot->pProcInitialise = InitialiseRobotCleaner;
          GET.pSlot->pProcCollision = CollisionRobotCleaner;
          GET.pSlot->pProcControl = ControlRobotCleaner;


Now, as usual, we have to try in game how last changes work.



Ok it works, now we have to do move it and for this target we can use a code very alike to that we did in Exercise 2: Lost in Space to move the cube with robotic movements.


To do move the Robot Cleaner

We've already seen the code to move in the space an object, avoding obstacles.
We can use that code but we have to make some change, since in that case we used a progressive action and we had saved some variables in the structure of our progressive action.
We should find another side where store these values.
We cann't use local variables, because their values have to be kept for all time.
We could use some global variable in MyData, but this solution it's not good, because we could have many cleaner robots and not only one.
The best choice is to save these variables own in the item structure.

Customizable variables of Item Structure

Fortunately, the item structure, StrItemTr4, has own some fields, not used by default, that you can use as you wish.

          short Reserved_34;                    // 34
          short Reserved_36;                    // 36
          short Reserved_38;                    // 38
          short Reserved_3A;                    // 3A

The above variables of StrItemTr4 structure, are free, we can use them to store those values that in old code we saved in progressive action.


Erratum about old code for robotic movement

In the code of exercise 2 for Robotic Random Movemement , we put some mistake using variable of progressive action when we can use common local variable for same target.
To understand when you can use (temporary) local variables, or when you have to use global variable (like are also those of structure of progressive acton or item structure), it's necessary wondering: this variable will be set (writting a value into) and used that value in same function and in some run-time?
Because if the answer is "YES", we can use a local variable to save that temporary value.
Differently, when you set a value in this variable, in one function and then we'll use it in another function, OR, when we set and read the value in same function, but in different times (i.e. we set now a value in a variable, we return from the function, and only in next frame, when it will be called newly, we'll read that variable) in these cases we'll have to use global variables.

In exercise 2 we had this code:

                    if (FLOOR.SlopeType == enumSLOPE.FLAT) {
                              // next sector is FLAT, set wished vertical orienting as 0x0000
                              pAction->VetArgShort[1] = 0;
                    }else {
                              // next sector is NOT flat, so it will be a slope.
                              // we discover if it is a rise or a declivity
                              // if the direction of the slope is the same of that of our object, then it is a "rise"
                              if (FLOOR.SlopeOrienting == GET.pItem->OrientationH) {

                                        // it is a rise
                                        // set wished value for rise
                                        pAction->VetArgShort[1] = 0x0999;
                              }else {
                                        // otherwise it will be a declivity
                                        pAction->VetArgShort[1] = -0x999;
                              }
                    }

                    // if the two vertical orienting, current and next, are the same, we have not to change anything
                    // and so we'll set as 0 the increment to change vertical orienting
                    if (GET.pItem->OrientationV == pAction->VetArgShort[1]) {
                              pAction->VetArgShort[2] = 0;
                    }

                    if (GET.pItem->OrientationV > pAction->VetArgShort[1]) {
                              // current vertical orienting is greaten than final: we have to use a negative increment to reach it
                              pAction->VetArgShort[2] = -pAction->VetArgShort[0];
                    }

                    if (GET.pItem->OrientationV < pAction->VetArgShort[1]) {
                              // current vertical orienting if less than final: we have to use a positive increment to reach it
                              pAction->VetArgShort[2] = pAction->VetArgShort[0];
                    }

                    // change the vertical orienting
                    GET.pItem->OrientationV += pAction->VetArgShort[2];

In above code we can using simple local variables for value of "wished vertical orienting" (pAction->VetArgShort[1]), and for "Increment to reach wished vertical orienting" (pAction->VetArgShort[2]), because both variables have been set and read in same function and in same run-time.
So, now, for Cleaner Robot, we'll replace these two "progressive" variables with two local variables, declared in ControlRobotCleaner() function:

          short VerticalOrientWished;
          short VerticalOrientInc;          

Please, note that for variable "pAction->VetArgShort[1]", with absolute value of increment to rotate vertically the cube in the distance of one sector, we cann't use a local variable, because we have set that value in Flipeffect800 code, and then we used it in the code to perform progressive action.
In this situation we'll have to use one of "reserved" variable of structure item, and we'll set that value in InitialiseCleanerRobot() function, to compute that value only once.
Also the speed we set in "pAction->Arg2" variable, requires a global variable, so we'll use another reserved variable of item structure:
So these two "global" variables, we'll be initialised, only once at begin, in the InitialiseRobotCleaner() function:

          // initialise horizontal speed
          GET.pItem->Reserved_36 = 32;

          // initialise the value to add to current vertical orienting to reach the change in the time to move the
          // robot for one sector, and save it to ReseReserved_34 variable of item structure
          FramesForSector = 1024 / GET.pItem->Reserved_36;
          GET.pItem->Reserved_34 = 0x0999 / FramesForSector; // absolute increment for vertical turning

Note: we cann't use the pItem->SpeedH variable to host "our" speed, because the SpeedH variables is strongly hardcoded. It will be set with the value taken from structure of current animation and AnimateItem() function will use the SpeedH value to do move the object in current direction (facing).
Since we are using a selfmade code to to move the object, we cann't use SpeedH to avoid conflicts between the two functions: ours and that of AnimateItem() function.


Summay of changes about variables from old code to new code

Changed Variables

Old Variable New Variable Description
pAction->VetArgShort[0] pItem->Reserved_34 This value is the absolute increment value, to turn vertically the cube of degrees about a slope of one click, in same number of times (frames) required to move the object for one sector
pAction->VetArgShort[1] VerticalOrientWished This variable has been converted as local variable. The value is the vertical orienting that is realtive to next sector in front of the object.
pAction->VetArgShort[2] VerticalOrientInc This variable has been converted as local variable. This value is the signed increment to add to current vertical orient of the object to reach the wished (final) vertical orient, in N times, where N is the number of frame required to the object to cover the distance of one sector
pAction->Arg2 pItem->Reserved_36 The increment to move the object in horizontal direction for each frame, i.e. the horizontal speed




New release of Robotic Movement code

Keeping in mind the changes about variables of previous paraghaph, we can copy and paste that old code and change it.
So we'll have following ControlRobotCleaner() function:

void ControlRobotCleaner(short ItemIndex)
{
          bool TestOk;
          short Direction;
          int IncX;
          int IncZ;
          short VerticalOrientWished;
          short VerticalOrientInc;


          Get(enumGET.ITEM, ItemIndex,0);

          if (TriggerActive(GET.pItem)==false) return;
          
          AnimateItem(GET.pItem);

          // code to move the robot

          Direction = 0;

          TestOk=false;

          // try to continue to move forward
          if (IsFreeWay(ItemIndex, Direction, GET.pItem->Reserved_36 ) == true) {
                    // it is possible continue to move forward
                    TestOk=true;
          }

          if (TestOk==false) {
                    // it was not possible to move forward: try to turn at left
                    Direction= -16384; // (- 0x4000 90 degrees at west);
                    if (IsFreeWay(ItemIndex, Direction, GET.pItem->Reserved_36 ) == true) {
                              // it is possible turn at left
                              TestOk=true;
                    }
          }

          if (TestOk==false) {
                    // it was not possible move forward and neither turning at left: try to turn at right
                    Direction = 16384;
                    if (IsFreeWay(ItemIndex, Direction, GET.pItem->Reserved_36 )==true) {
                              // it is possible turn at right
                              TestOk=true;
                    }
          }

          if (TestOk == false) {
                    // it has been NOT possible move in any direction: to do explode the cube
                    PerformActionTrigger(NULL, 14, ItemIndex, 2);
                    return;
          }

          // it's possible to do move the cube
          // it the (relative) direction is different than 0 (0=continue forward), we'll have to change also
          // the facing of the cube
          if (Direction != 0) {
                    GET.pItem->OrientationH += Direction;
          }

          if (FLOOR.SlopeType == enumSLOPE.FLAT) {
                    // next sector is FLAT, set wished vertical orienting as 0x0000
                    VerticalOrientWished = 0;
          }else {
                    // next sector is NOT flat, so it will be a slope.
                    // we discover if it is a rise or a declivity
                    // if the direction of the slope is the same of that of our object, then it is a "rise"
                    if (FLOOR.SlopeOrienting == GET.pItem->OrientationH) {
                              // it is a rise
                              // set wished value for rise
                              VerticalOrientWished = 0x0999;
                    }else {
                              // otherwise it will be a declivity
                              VerticalOrientWished = -0x999;
                    }
          }

          // if the two vertical orienting, current and next, are the same, we have not to change anything
          // and so we'll set as 0 the increment to change vertical orienting
          if (GET.pItem->OrientationV == VerticalOrientWished) {
                    VerticalOrientInc = 0;
          }

          if (GET.pItem->OrientationV > VerticalOrientWished) {
                    // current vertical orienting is greaten than final: we have to use a negative increment to reach it
                    VerticalOrientInc = -GET.pItem->Reserved_34;
          }

          if (GET.pItem->OrientationV < VerticalOrientWished) {
                    // current vertical orienting if less than final: we have to use a positive increment to reach it
                    VerticalOrientInc = GET.pItem->Reserved_34;
          }

          // change the vertical orienting
          GET.pItem->OrientationV += VerticalOrientInc;

          // now compute the new position of the item using its facing because we have already changed it if it was
          // necessary
          GetIncrements(GET.pItem->OrientationH, &IncX, &IncZ, GET.pItem->Reserved_36 );

          GET.pItem->CordX += IncX;
          GET.pItem->CordZ += IncZ;
          // in the case the cube was over a slope we have to change also its Y coordinate, setting it to same
          // value of floor height
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          GET.pItem->CordY = FLOOR.FloorHeight;

          // we have to update room of object in the case it was changed
          UpdateItemRoom(ItemIndex);

}

Now we'll build the project and try in game how robot works...

The limits of moveable collision boxes



We have a bad problem.
The robot cleaner move in the labyrinth about how the old cube, but only "about" because now we detect a problem that was missing in old cube code.
In above picture you see the problem: after only some change of direction the cleaner robot seems misaligned respect to the floor (the rail way) where it should move.
What's happened?
But the real question is another: why have we had this bug with robot cleaner and not with the cube, when we have used absolutely the same code?
The answer is about the box collisions.
While the cube had no animations and the collision box was perfectly aligned with the mesh shape: always 1024x1024x1024 game units, with the robot cleaner the collision box change for each frame in according with position of its meshes (the turning cables)


If you look carefully above image you see that the collision box in A picture is different than that in B picture.
In old code the IsFreeWay() function, used the collision box to compute the distance between the pivot of the object and its boundary in the direction of movement.


If the collision box is larger (or misplaced: moved forward) respect to its 1024x1024x1024 volume, the IsFreeWay() function will detect the presence of an obstacle (in next sector) before the cube was really very closed to it (see above B picture) and the change of direction (C picture) it will happen early, in advance, loosing the right alingment with center of the sectors of our ideal railway.

How to fix the bug about alignment with floor rail-way

There are three methods to fix this problem:
  1. Correct all collision boxes, frame by frame, of the animation.
    We could verify and fix all collision boxes for each frame, to be sure that they were always placed at center of object pivot, and they had always the size of one sector 1024x1024 on x,z plane

  2. Perform the check for obstacles in next sector ONLY when the robot is at center of current (previous) sector, in this way when we'll detect an obstacle and we'll have to turn it, we know that it is already at center of the sector and so also to the new rail-way direction.

  3. Change the IsFreeWay() function to do it accepted a fixed half/width of the object, skipping the reading of current collision box from animation data.

All above methods should work, anyway:
The first method, change the collision boxes for all frames, is very long and boring to do.
The second method is the most sure for final result, but in this case we'll do no check for intermediate positions of the robot but we'll perform that control only once for the sector and this is a bit weird and it could create problem for our control about change of vertical orienting where we have to check the floor on next sector continuosly to update the vertical turning.
The third method is that we'll use, because it will get more efficient the IsFreeWay() function and it will preserve the good working of old code of robotic movement, since at end it will work exaclty in same way, because we'll force the same half/width for robot as it was for the cube.

Improvement of IsFreeWay() function

We have to add a new input argument to pass the fixed half/width to use, and we need also of another argument for topside Y to use about detection of collision with ceiling, because the problems with collision boxes of animation are present also for the height of the object.


Looking above picture you see that the robot detect the ceiling in front (A picture) as an obstacle, and it turns at its left (B picture)
The reason is the collision box became very higher for the electric cables but we could wish ignore that kind of collision and let that the robot was able to enter in that tunnel.
So we'll have to add also an argument for TopSideY:
For this reason we'll change the declaration of IsFreeWay() function, from old declare:

bool IsFreeWay(int ItemIndex, short Direction, int Distance);

To new declare with two new arguments:

bool IsFreeWay(int ItemIndex, short Direction, int Distance, int HalfWidth, int TopSideY)

The new arguments: "HalfWidth" and "TopSideY" have the same names of previous local variables defned inside of the IsFreeWay() body:

          short AbsDir;
          short OppositeDir;
          int HalfWidth;
          int TopSideY;

So, we'll have to remove those two local variables because now they are input arguments.

          short AbsDir;
          short OppositeDir;

The ideal situation is to allow to the builder (to us), to choose everytime if we wish use the collision box of the moveable, or we prefer force the size of the object typing its values as arguments of IsFreeWay() function.
A way to reach this target is to set two values for these two arguments, that they will mean: "this time compute the collision from moveable and ignore these (absurd) values"
Like in script commands we used the "IGNORE" to let a default preset, we can use a value that is not valid like HalfWidth, for instance own the value -1 since it's not possible that the size of an object was a negative value.

So when we'll use our new IsFreeWay() function, and we wish supply the values about size of the object, we'll type these (acceptable) values in the call of the function, while when we wish let it was the function to compute these sizes, we'll type -1 as HalfWidth input argument.
Now we can change the code of IsFreeWay() function, following the new syntax:

bool IsFreeWay(int ItemIndex, short Direction, int Distance, int HalfWidth, int TopSideY)
{
          int IncX;
          int IncZ;
          int NewX;
          int NewY;
          int NewZ;
          short AbsDir;
          short OppositeDir;

          Get(enumGET.ITEM, ItemIndex, 0);
          AbsDir = GET.pItem->OrientationH + Direction;

          if (HalfWidth == -1 {
                    // the user has not set a valid size for the object, so now it's necessary compute the size byself:
                    GetItemSize(ItemIndex, &TopSideY, &HalfWidth);
          }
          GetIncrements(AbsDir, &IncX, &IncZ, Distance+HalfWidth);

While the following code it will be the same of old version.

We added a condition before computing the size of the object using moveable collisions:

          if (HalfWidth == -1) {
                    GetItemSize(ItemIndex, &TopSideY, &HalfWidth);
          }

Now we'll compute the size ONLY if the supplied value for HalfWidht is -1 (an unvalid value), while in the case it was different, this means that the source code called IsFreeWay() function, suppling to it valid values for HalfWidth and TopSideY variables, so IsFreeWay() will use these values, omitting the computing byself of collision box.
Now we have to change the code where we used IsFreeWay() function, because now there are two arguments to set, and if you don't change the calls supplying these two extra argument Visual Express will give following error:

error C2660: 'IsFreeWay' : function does not take 3 parameters

Because now IsFreeWay takes 5 parameters.
About these changes we could work in this way:
For the calls about old code to move the cube, we can supply -1 (our IGNORE value) to do work it like in old code, computing byself the size of the object, since we know that it worked fine for the cube.
While when we used IsFreeWay() for our robot cleaner, we'll set as HalfWidth 511 and as TopSideY parameter the value to mean an height from floor of 1023 game units.
About the TopSideY the value cann't be an absolute value because we have to supply the Y absolute coordinate in 3d world of this top side y position.
So we'll have to add to BottomY coordinate of robot the negative value (negative, to move upper in the space) "-1023".

Note: to reach the rows with errors to fix in Microsoft Visual Express, an easy way is to perform a double click with the mouse where you see the error message:

error C2660: 'IsFreeWay' : function does not take 3 parameters

In this way Visual Express will show to you the source and right line where there is that error to fix.


Change the calls for new IsFreeWay() function

As we said, we'll change the calls of IsFreeWay() for progressive action of the cube, adding two "-1" values, to preserve old computation to get byself the size of the object (cube)
While for the calls of our Cleaner Robot we'll add these two parameters: 511 as HalfWidth, and this formula as TopSideY:

          TopSideY= GET.pItem->CordY - 1023;

And then we'll use TopSideY (local variable) to pass the value for TopSideY argument of IsFreeWay() function:

          TopSideY= GET.pItem->CordY - 1023;

          // try to continue to move forward
          if (IsFreeWay(ItemIndex, Direction, GET.pItem->Reserved_36, 511, TopSideY ) == true) {
                    // it is possible continue to move forward
                    TestOk=true;
          }

          if (TestOk==false) {

                    

                    // it was not possible to move forward: try to turn at left
                    Direction= -16384; // (- 0x4000 90 degrees at west);
                    if (IsFreeWay(ItemIndex, Direction, GET.pItem->Reserved_36, 511, TopSideY ) == true) {
                              // it is possible turn at left
                              TestOk=true;
                    }
          }

          if (TestOk==false) {
                    // it was not possible move forward and neither turning at left: try to turn at right
                    Direction = 16384;
                    if (IsFreeWay(ItemIndex, Direction, GET.pItem->Reserved_36, 511, TopSideY )==true) {
                              // it is possible turn at right
                              TestOk=true;
                    }
          }


Now we try with last changes...



Ok, now it works fine, the robot is well aligned with floor sectors and it is able to enter in the tunnel.

Kill Lara on contact

Now we have to get dangerous our Cleaner Robot.
Just we kill lara when Lara touches the robot.
Since we have already discovered the moment of the collision, just only adding an instruction to remove all health of lara.
In the CollisionRobotCleaner() function, when we detect a contact, and we have called the ItemPushLara() function, we add also the removing of Lara's health.

          // lara and robot are in collision
          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);
          // kill lara
          pLara->Health = 0;

Now try in game how lara dies...


It works but it's not so fine to see.
It seems that, when lara died, the ItemPushLara() stops to work correctly, and the Lara's body is swallowed by robot shape. See above pictures.
We have some solutions for this problem:

  1. We can force an animation for Lara where she will be thrown away and standing on the ground immediatly.

  2. We could simulate the same behaviour of ItemPushLara(), moving the Lara's body in same direction of the robot

  3. We can stop the robot when it killed Lara, like if the crash had damaged also the robot.

We'll use the third solution, because it is easier and because we saw other times in tomb raider game, for example when enemies, after have killed lara, move aways ignoring her body.
In this case we can stop the robot while lara is dying.


How to stop the movement of the Robot Cleaner

Stop the movements of the robot is very easy: just simply suspend the execution of the code in ControlRobotCleaner() function when Lara has health < 1.
So we type in ControlRobotCleaner() function following code:

          AnimateItem(GET.pItem);

          // code to move the robot
          Get(enumGET.LARA,0,0);
          if (GET.pLara->Health < 1) {
                    // lara is dead, stop movements of the robot
                    return;
          }

Note: we let the call about AnimateItem() function, to let that the cable contineds to turn, since that is not a problem, while we quit the code when lara died, in this way the robot will stop in the moments and we'll avoid that it cover the body of Lara.

Build & try in game what happens...



It's better now, more realistic, let's say.


How to add special effects to the killing of Lara

The last killing of Lara is good but it's a bit too calm and quiet...
To simulate the crash of the robot (and its damage that got it out of order) we could add some special effects.
We could use some trng triggers to simulate a short heartquake and a flash of light.
We should add this code when we detect the collision and so the death of Lara, but we need to perform only once this triggering.
If we performed this code simply when Lara and robot is colliding, this situation will be repeated for some seconds, because the game doesn't stop immedialtely when Lara died.
So, to idenitify a single one frame to trigger a single one effect, we have to detect the specific frame where we killed lara, ignoring the others when lara had been already killed but the game will go on.
In CollisionRobotCleaner() function we'll change the code in this way:

          // lara and robot are in collision
          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);
          if (pLara->Health > 0) {
                    // lara is yet living: only now, in this frame we'll kill her
                    // kill lara
                    pLara->Health = 0;
                    // trigger an earthquake
                    PerformFlipeffect(NULL, 7, 30, 0);
                    // perform a yellow flash of the screen
                    PerformFlipeffect(NULL, 355, 2, 10);
          }

Note: we perform the flipeffect only when Lara was yet alive, in this way we'll trigger only one earthquake, and only one flash of the screen, since in next frame we'll find lara already dead, with health = 0

The

PerformFlipeffect(NULL, 1, 0, 0);

It is the [Export Function] of flipeffect:

; Set Trigger Type - FLIPEFFECT 1
; Exporting: TRIGGER(0:0) for FLIPEFFECT(1) {Tomb_NextGeneration}
; <#> : OldFlip. Plays a fast single eartquake, sound and rumbler.

; <&> :
; (E) :


While the other flipeffect:

PerformFlipeffect(NULL, 355, 2, 10);

It has been got from exporting of function for following trigger:

; Set Trigger Type - FLIPEFFECT 355
; Exporting: TRIGGER(2562:0) for FLIPEFFECT(355) {Tomb_NextGeneration}
; <#> : Screen. Flash screen with the <&>Light color for (E)Durate
; <&> : Yellow
; (E) : Fast



How to add sparks effect




Usually a "cleaner" should not kill anyone, but our cleaner robot is dangerous because it has cables with opened contacts with eletric power.
In above A picture you see these metal contacts (in red circles).
We should add sparks effect to these contacts to get evident the presence of energy and its danger.
We have also a problem. In B picture you see that very often these contacts will be inside of the wall or the floor, because they are too long for the space where they turn.
If we apply the spark where there are metal contacts (A picture), in many circustances the sparks will be not visible, because inside of the wall/floor.
We'll have to solve this little problem, anyway now we see how to add sparks for our robot.


The TriggerFlareSparks() tomb4 function
This is a tomb4 function and it has a lot of arguments:

typedef void (__cdecl *TYPE_TriggerFlareSparks) (int CordX, int CordY, int CordZ, int Red, int Green, int Blue, DWORD TestSmoke, DWORD Unused);

CordX, CordY, CordZ arguments
--------------------------------------------
First three arguments give the position in 3d world where drawing the sparks

Red, Green, Blue arguments
--------------------------------------
We can choose the color of the sparks, suppling the three color intensity.

TestSmoke argument
----------------------------
In spite of its name I'm not sure it was own for the smoke.
When you set "1" as TestSmoke the number of sparks will be increased but these extra sparks will be littler, giving the idea of dust or ... smoke.

Unused argument
-----------------------
Oddly it seems that this argument had no effect, in spite in tomb4 code it has been often set = 1.


The GetJointAbsPosition() tomb4 function
To locate the position of a single mesh in some moveable we can use the GetJointAbsPosition() tomb4 function.
In spite of its name, where there is the "joint" word, I believe it worked on coordinate of a single meshe owned by the object.
Only Lara object has well defined "joints".

typedef void (__cdecl *TYPE_GetJointAbsPosition) (StrItemTr4 *pItem, StrMovePosition *pCoordinate, int JointIndex);


pItem argument
---------------------
This is the pointer of an item structure. It will be the owner of mesh whom we wish discover the position

pCoordinate argument
-----------------------------
This is another pointer, this time to the StrMovePosition structure.


typedef struct StrMovePosition {
          int RelX;
          int RelY;
          int RelZ;
}MovePositionFields;

This means that we'll have to declare a local variable for this kind of structure and then to pass to the function its addres (the pointer).

          StrMovePosition MyPos;

          GetJointAbsPosition(GET.pItem, &MyPos, 2);

Anyway in this case the structure should be not let empty, we have to set its fields (RelX, RelY, RelZ) as displacements (offsets) respect that it will be the absolute position of the mesh we are looking for.
For instance if we wish have as returned values ONLY the exact position of the mesh, we'll set all "0" in MyPos strucure before calling the function:

          MyPos.RelX =0;
          MyPos.RelY =0;
          MyPos.RelZ= 0;
          GetJointAbsPosition(GET.pItem, &MyPos, 2);

And after the calling, we'll find in MyPos structure the new values, the absolute coordinate of mesh "2" (in above example) for pItem moveable.
If we wish have a change in relative direction, we can set in MyPos structure, before calling the function, these displacement.
For instance if we wish have the point moved respect to mesh2, forward respect horizonal orienting of the object, of 100 game units and upper in 3d world of 50 game units you should use this code:

          MyPos.RelX = 0;
          MyPos.RelY = -50;
          MyPos.RelZ = 100;
          GetJointAbsPosition(GET.pItem, &MyPos, 2);


How to add an effect at given mesh

To locate the position in 3d space where to show our sparks we'll use the GetJointAbsPosition(), but we have to know also the number (index) of the mesh to use as input for this function.
Using Animation Editor of Wad Merger program, we can see the object.


Clickin in frame named "meshes", on same number of mesh, we can see highlighted that mesh and so discover its index.
Performing this operation for all meshes that are at end of electric cables, we discover all indices we need: 5, 9 and 13.
We'll use these values as JointIndex parameter for GetJointAbsPosition() function.
Since we'll perform same operation for all (three) cable mesh, it's better creating a function, where we'll pass the parameter for moveable structure of ower object, and the index of the mesh, in this way we'll type the code only once, and then we'll call three times this function for the three meshes.
Another advantage to have a function is that in this way we could use newly this code (function) also in other situation with other moveables, everytime we'll need to add sparks to some moveable.


The function AddSparksEffect() function

Using the two tomb4 function we had just seen, it's easy add the sparks effect closed to given mesh.
This is the code of AddSparksEffect() function:


void AddSparksEffect(StrItemTr4 *pItem, int MeshIndex, int OffX, int OffY, int OffZ)
{
          StrMovePosition MyPos;

          MyPos.RelX = OffX;
          MyPos.RelY = OffY;
          MyPos.RelZ = OffZ;
          // locate position of MeshIndex:
          GetJointAbsPosition(pItem, &MyPos, MeshIndex);

          // show sparks in position we just got
          // (note: we use as color the yellow: rgb(242,251,0)
          TriggerFlareSparks(MyPos.RelX, MyPos.RelY, MyPos.RelZ, 242, 251, 0, 0, 0);
}


Now we can call this function for each of three "cable" meshes.
We'll type this code in the ControlRobotCleaner() function:

          // we have to update room of object in the case it was changed
          UpdateItemRoom(ItemIndex);

          AddSparksEffect(GET.pItem, 5, 0,0,0);
          AddSparksEffect(GET.pItem, 9, 0,0,0);
          AddSparksEffect(GET.pItem, 13, 0,0,0);
}

With above code we added sparks to three meshes of robot cleaner, the mesh: 5, 9 and 13
Now we try in game how it works.

It doesn't work fine



In game the result of our sparks is not good.
It's very difficultous see the sparks, you can see those very little sparks in red circles of above image.
The AddSparksEffect() is not the problem, the problem is how we have used it.
If we apply the sparks on a mesh that is moving in two way: turning on its axis (the cables) and moving forward the robot, the source of sparks will be never the same, since from one frame and the next, the position changed too quickly and the density of sparks is too low in a single point of 3d space to be visible.
We can solve this problem in two way:

  1. Increasing the number of sparks to emit for each frame, calling many times the TriggerFlareSparks() function for each frame.

  2. Freezing the point where to show the sparks. In this way, when we discover a point of 3d space, we'll keep the same point also for many frames, in this way the sparks will not follow always the movement of the meshes and of the robot

Since the sparks are a limited resource, we'll use second method.
More, with this method, we could fix also other problem we had described previously.


The position where show the sparks (on the meshes of final cables) it very often inside of the walls, floor or ceiling, and this is a wasting of resouces.
We'll have to compute the position where the cable enter in wall/floor/ceiling (see picture above, right side), and we'll show for some number of frames, for instance half of second (15 frames) all sparks in that point.
This means also that we'll not emit sparks in any position of turning meshes, in this way the sparks will be emitted only at random interval, while the cable touch the wall, that is also a good saving of resource and more realistic: when the electrified cable touch the wall/floor/ceiling, there are sparks, while when they are not touching anything, there will be no sparks.
This method is a bit complicated of course, but it will be useful to learn how to work better with boundaries of room geometry.

The CheckFloor() function in depth



To solve our problem we need to know better how the CheckFloor() function works...
In above picture you can see some situations we should be able to manage.
The A point is under the floor, while the B point is inside a wall.
The CheckFloor() will give different results about these two points.
While for B point the CheckFloor() function will return in FLOOR structure the value TestFullWall=true. And you'll discover this situation with the code:

          if (FLOOR.TestFullWall == true) {
                    // the point is inside a wall
          }

In the case of A point, the TestFullWall will be = false, because in that sector, of that room, there is some empty (free) space. So the FLOOR structure will have valid values for FloorHeight and CeilingHeight variables.
The FloorHeight will have the value of Y coordinate of C point in above picture.

Another interesting data of the sector where falls the point we supplied to CheckFloor() function, is that about the sector boundaries.
In FLOOR structure there is a sub-structure named "SectorCoords" with following variables:

          DWORD WestZ; // lower Z coordinate (west side of the sector)
          DWORD EastZ; // higher Z coordinate (east side of the sector)
          DWORD NorthX; // lower X coordinate (north side of the sector)
          DWORD SouthX; // higher X coordinate (south side of the sector)
          DWORD MiddleX; // middle point of the sector
          DWORD MiddleZ; // x and z coordinate
          int Radius; // distance from middle point of sector respect to the source point analysed in CheckFloor()



In above picture you can discover the meaning of these variables.
All checked points showed in above pictures, A, B, C, D and E, will give same results about "SectorCoords" because the sector is always the same.
The only value that will change in according with the checked point, is the "Radius" value that is the (horizontal) distance between the point (E in above picture) and the middle point of the sector.


Another progressive action: AXN_ADD_EFFECT

To keep for some frame the same point where to draw sparks we need of another progressive action.
We can call it: AXN_ADD_EFFECT, so we can also using it for other effects other that sparks.
Everytime we create a new progressive action we have to create a new AXN_ constants, and then we should set what variables of progressive action structure we'll use and what it will be their meaning.
So we create our new constant: AXN_ADD_EFFECT:

#define AXN_FREE 0 // this record is free. You type this value in ActionType to free a action progress record
#define AXN_MOVE_CUBE 1
#define AXN_ADD_EFFECT 2

And we plane the variables to use:



The changes at AddSparksEffect() function

We have to change the code of AddSparksEffect() function.
It's necessary replace the call to TriggerFlareSparks() function, with the code to launch our progressive action (AXN_ADD_EFFECT) to draw the sparks in the given position for some frames (15 as first experiment)

void AddSparksEffect(int ItemIndex, int MeshIndex, int OffX, int OffY, int OffZ)
{
          StrMovePosition MyPos;
          
          Get(enumGET.ITEM, ItemIndex, 0);

          MyPos.RelX = OffX;
          MyPos.RelY = OffY;
          MyPos.RelZ = OffZ;
          // locate position of MeshIndex:
          GetJointAbsPosition(GET.pItem , &MyPos, MeshIndex);

          // show sparks in position we just got
          // (note: we use as color the yellow: rgb(242,251,0)
          Get(enumGET.PROGRESSIVE_ACTION, 0,0);

          GET.pAction->ActionType = AXN_ADD_EFFECT;
          GET.pAction->Arg1 = 15; // 15 frames
          GET.pAction->Arg2 = EFF_ADD_SPARKS;
          GET.pAction->ItemIndex = ItemIndex;
          GET.pAction->VetArgBytes[0] = 242; // red
          GET.pAction->VetArgBytes[1] = 251; // green
          GET.pAction->VetArgBytes[2] = 0; // blue

          // type coordinates
          GET.pAction->Coords.To.OrgX = MyPos.RelX;
          GET.pAction->Coords.To.OrgY = MyPos.RelY;
          GET.pAction->Coords.To.OrgZ = MyPos.RelZ;
}


Note: it's been necessary change also the input arguments, replacing the "StrItemTr4* pItem" with the index of the item, because now we have to save the index in the progressive action.
This means that we should change also the calls to AddSparksEffect() from ControlRobotCleaner() function:

          // we have to update room of object in the case it was changed
          UpdateItemRoom(ItemIndex);

          AddSparksEffect(ItemIndex, 5, 0,0,0);
          AddSparksEffect(ItemIndex, 9, 0,0,0);
          AddSparksEffect(ItemIndex, 13, 0,0,0);
}


The code of AXN_ADD_EFFECT progressive action

Now we type the code for the AXN_ADD_EFFECT progressive action.
So we go in PerformMyProgrAction() function, and we add the "case AXN_ADD_EFFECT:" to add some effect

          case AXN_ADD_EFFECT:
                    // see what kind di effect we have to add
                    if (pAction->Arg2 == EFF_ADD_SPARKS) {
                              // add sparks
                              TriggerFlareSparks(pAction->Coords.To.OrgX,
                                                                                pAction->Coords.To.OrgY,
                                                                                pAction->Coords.To.OrgZ,
                                                                                pAction->VetArgBytes[0], // red
                                                                                pAction->VetArgBytes[1], // green

                                                                                pAction->VetArgBytes[2], // blue
                                                                                0, 1);

                    }
                    // decrease number of frames for durate effect and check if we completed
                    if (pAction->Arg1 != ENDLESS_DURATE) {
                              pAction->Arg1--;
                    }
                    if (pAction->Arg1==0) {
                              // completed: quit the action
                              pAction->ActionType= AXN_FREE;
                              break;
                    }
                    break;

          case AXN_MOVE_CUBE:

Note: it's indifferent if we place the "case AXN_ADD_EFFECT:" above or belove the previous "case AXN_MOVE_CUBE:", just remembering always to close the code with the "break;"


It has been increased the number of sparks but ...



Now there are more sparks but the problem about its dispersion for the fast movements it is yet present.
Now we have to locate only the point where a cable touch walls, floor or ceiling and concentrate in that point the spark emission.


How to find the borders of current sector



We have already seen above image, to describe some values returned by CheckFloor() function that we can find in FLOOR.SectorCoords sub-structure.
Now we see how to discover when the final meshes of the cable (the numbers: 5, 9 and 13)
The logic is this:


The new AddSparksToCables() function

Since now we add sparks changing the position from final mesh to move to sector borders we cann't use the previous AddSparksEffect() function.
We have to create a new function, to use only for our cleaner robot or other situation where we wish sparks only on contact between a mesh and the room geometry.
We call it "AddSparksToCables".

void AddSparksToCables(int ItemIndex, int MeshIndex)
{
          StrMovePosition MyPos;
          bool TestDrowned;

          Get(enumGET.ITEM, ItemIndex, 0);

          MyPos.RelX = 0;
          MyPos.RelY = 0;
          MyPos.RelZ = 0;
          // locate position of MeshIndex:
          GetJointAbsPosition(GET.pItem , &MyPos, MeshIndex);
          // now we analys the situation in that 3d point where we should add sparks
          CheckFloor(MyPos.RelX, MyPos.RelY, MyPos.RelZ, GET.pItem->Room);
          // we'll have to remember if the point is drowned in some wall (or floor or ceiling) or less
          TestDrowned=false;

          // if the point is on air (it's not "drowned" by the walls) we'll not add sparks
          if (FLOOR.TestFullWall == false) {
                    // the point is NOT inside a wall, but it could be under floor or above the ceiling
                    // note: the cable don't reach the floor, so we'll accept when the coordinate of the mesh
                    // is very closed to the floor
                    if (AbsDiffY(FLOOR.FloorHeight,MyPos.RelY) < 30) {
                              // the point is very closed to the floor: now we change the coordinate to be a bit over the floor
                              MyPos.RelY = FLOOR.FloorHeight - 10;
                              TestDrowned=true;
                    }

                    if (FLOOR.CeilingHeight > MyPos.RelY) {
                              // the point is above the ceiling. Now we change the coordinate to be a bit belove of the ceiling
                              MyPos.RelY = FLOOR.CeilingHeight + 10;
                              TestDrowned=true;
                    }

                    // if the point was NOT droned we quit (no emission of spark today)
                    if (TestDrowned==false) return;
                    
          }else {
                    // the point is drowned in a wall
                    TestDrowned=true;
                    // now we have to discover a point at same height of original position but that it was in the sector
                    // where there is the cleaner robot so to get visible the sparks
                    // since the wall it will be surely a side (at left or right) of robot, respect at its direction
                    // we discover what is its direction and then we discover how change original position of the
                    // point to get one that it was visible at borders of sector of the robot
                    // note: Since when the point of CheckFloor() is inside a wall, all other data of FLOOR structure
                    // will be NOT valid, we have to use the sector where there is the robot and work on its boundaries
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);

                    if (GET.pItem->OrientationH == enumORIENT.EAST ||
                              GET.pItem->OrientationH == enumORIENT.WEST ) {
                              // robot is moving following Z axis. so we'll have to change the X coordinate of original point
                              // taking that south or at north in according if it is lower or higher than X coordinate of robot

                              if (GET.pItem->CordX > (DWORD) MyPos.RelX) {
                                        // the cable was at north of the robot. we'll choose the north side its sector
                                        MyPos.RelX = FLOOR.SectorCoords.NorthX + 10;


                              }else {
                                        // the cable was at south of the robot. We use the south side of the sector
                                        MyPos.RelX = FLOOR.SectorCoords.SouthX - 10;
                              }

                    }else {

                              // the robot is moving on X axis, so we'll have to change the Z coordinate of original point
                              // taking the side at east or west of its sector
                              if (GET.pItem->CordZ > (DWORD) MyPos.RelZ) {
                                        // the cable is at west of the robot: we'll take the west side of its sector
                                        MyPos.RelZ = FLOOR.SectorCoords.WestZ + 10;
                              }else {
                                        // the cable is at east of the robot: we'll take the east side of its sector
                                        MyPos.RelZ = FLOOR.SectorCoords.EastZ - 10;
                              }
                    }

          }

          // se the point was not drowned we quit
          if (TestDrowned == false) return;

          // now we add the the spark: in MyPos structure there will be new coordinate where add sparks
          // show sparks in position we just got
          // (note: we use as color the yellow: rgb(242,251,0)
          Get(enumGET.PROGRESSIVE_ACTION, 0,0);

          GET.pAction->ActionType = AXN_ADD_EFFECT;
          GET.pAction->Arg1 = 15; // 15 frames
          GET.pAction->Arg2 = EFF_ADD_SPARKS;
          GET.pAction->ItemIndex = ItemIndex;
          GET.pAction->VetArgBytes[0] = 242; // red
          GET.pAction->VetArgBytes[1] = 251; // green
          GET.pAction->VetArgBytes[2] = 0; // blue

          // type coordinates
          GET.pAction->Coords.To.OrgX = MyPos.RelX;
          GET.pAction->Coords.To.OrgY = MyPos.RelY;
          GET.pAction->Coords.To.OrgZ = MyPos.RelZ;

}

Now we change the calls in ControlRobotCleaner() function, to call our new AddSparksToCables() function

          // we have to update room of object in the case it was changed
          UpdateItemRoom(ItemIndex);

          AddSparksToCables(ItemIndex, 5);
          AddSparksToCables(ItemIndex, 9);
          AddSparksToCables(ItemIndex, 13);
}

And now try the results...



The sparks are on the boundaries of the sector where is the robot.
Ok it is what we tried to get but it's not so fine to see, a bit weird.
We should having only some sparks and not a full box shaped of sparks.

The GetRandomControl() tomb4 function

It should be better if we had only some sparks in random way on some obstacle closed to the robot.
A fast way to introduce random events is to create a conditon with a generator of random numbers and then perform some action (like our emission of sparks) only when the range of number is enclosed in some value.
The GetRandomControl() function is a tomb4 procedure that has the advantage to be very fast to be performed.
This function returns a random number that could be very big. There is no way, in input, to select the range of random numbers returned, but we can to mask the returned big number to have a range of values that it will be always enclosed between 0 and a Power By 2 (-1)
Power by 2 are numbers like: 2, 4, 8, 16, 32, 64, 128, 256, 512 ect.
We can choose performing an binary AND with returned value.
For instance:

          int Value;

          Value = GetRandomControl() & 15;

And now Value will be a random number in the range 0 / 15

We'll use this chance to avoid a continue emission of sparks, to get a random emission.
So we change the calls for AddSparskToCable() function in this way:

          Value= GetRandomControl() & 7;

          if (Value ==0) {
                    
                    AddSparksToCables(ItemIndex, 5);
                    AddSparksToCables(ItemIndex, 9);
                    AddSparksToCables(ItemIndex, 13);
          }

In above code we got a random value from GetRandomControl().
We mask it using "& 15" to have only a number in the range 0 / 15
Then we perform a condition to add sparks only when this randon value = 0. The chance that it happened it is 1 on 16, for "0" like for any other value in the range "0/15"

Now we see what happens in game


Ok, the sparks effect sucks...



I don't want lying to you: this sparks effect sucks!
Anyway it's not so important. We learnt how to emit sparks, hot to compute the position about meshes of some moveable, and we did some computation to fix coordinates in according with sector boundaries.
It's enough for me.
Now try to take care about a more important matter...

How to have selective collisions

We have already set the code to detect a collision between Lara and the robot, in CollisionRobotCleaner() function.
When the Robot touches Lara, she will be killed.
It works enough fine, but if we suppose that the most dangerous side of the robot is that where there are the electric cables, we should fix this situation:


In above picture you can see what happens if lara touches the robot in its backward side: she died, as usual.
But if the dangerous side is that of the cables (forward side) it should be better if she didn't dye in that situation.
As exercise, we can change our code to get this result: Lara will be killed only if she is touching the forward side of the robot cleaner, while in other situations, we'll use the common collision to keep her away from the robot, also to avoid the bad situation of above C picture.

Theoratically we could use a long (for cpu time) control mesh for mesh, checking if one of the many cable meshes (they are 9, 3 for each cable) is touching Lara.
But we have a fast and easy way to verify this situation.
Since the robot moved only in hortogonal way, when we detect a collision between lara and the robot, we can verify if lara is in front of the robot (and she will be killed) or at side or backward, and she will be only puhsed away.
Now we read the code used to detect collision:

          if (TestCollision(GET.pItem, pLara)==false) return;

          // lara and robot are in collision
          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);
          if (pLara->Health > 0) {
                    // lara is yet living: only now, in this frame we'll kill her
                    // kill lara
                    pLara->Health = 0;

                    // perform a yellow flash of the screen
                    PerformFlipeffect(NULL, 355, 2, 10);
                    // trigger an earthquake
                    PerformFlipeffect(NULL, 1, 0, 0);
                    // play sound 183 THUNDER_CRACK on coordinate of robot
                    SoundEffect(183, &GET.pItem->CordX, 0);
          }

In above abstract, we should type some code, after "ItemPushLara()" function (that we'll use always), to verify if the collision is deadly or less.
We'll use this logic:

So we change the code in this way:

          // lara and robot are in collision
          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);
          // detect if lara is touching the forward side of the robot or less
          // we'll use the TestForward variable to remember the result of our controls
          TestForward=false;

          if (GET.pItem->OrientationH == enumORIENT.EAST) {
                    // the robot is moving on + Z axis
                    CablesCord = GET.pItem->CordZ + 490;
                    LaraCord = pLara->CordZ;
          }

          if (GET.pItem->OrientationH == enumORIENT.WEST) {
                    // the robot is moving on - Z axis
                    CablesCord = GET.pItem->CordZ - 490;
                    LaraCord = pLara->CordZ;
          }

          if (GET.pItem->OrientationH == enumORIENT.SOUTH) {
                    // the robot is moving on + X axis
                    CablesCord = GET.pItem->CordX + 490;
                    LaraCord = pLara->CordX;
          }

          if (GET.pItem->OrientationH == enumORIENT.NORTH) {
                    // the robot is moving on - X axis
                    CablesCord = GET.pItem->CordX - 490;
                    LaraCord = pLara->CordX;
          }

          if (AbsDiff(CablesCord, LaraCord) < 200) {
                    // lara and the coordinates of the cables are very closed
                    TestForward=true;
          }

Note: we had to add +/- 490 to the robot coordinate because the pivot of the robot is at center of its meshes while we wish know the position of the cables, those are in forward side. We set that the distance between center of the robot and the cables was about 490 game units.
We declared also three local variables to host our intermediate values:

          bool TestForward;
          DWORD CablesCord;
          DWORD LaraCord;

Now we change also the condition to kill lara, adding the further condition that the contact had to be happened in forward side, otherwise we'll not kill her:

          if (pLara->Health > 0 && TestForward == true) {
                    // lara is yet living: only now, in this frame we'll kill her
                    // kill lara
                    pLara->Health = 0;


Now we try in game...


Ok, from back it's salubrious



Now Lara can touches the robot from backward and from sides, just she keeps her aways from the cables in front of the robot.


How to detect collision between enemies

How we saw the Collision() management procedure works only to detect a collision between that moveable and Lara.
But what happens if an enemy enters in collision with another moveable?
It should happen that both enemies tried to avoid the collision, changin their direction.
Now we'll try to verify what happens when the robot cleaner enters in collision with another moveable.
First code it will be for a not so "moveable" item: the pushable item.




Before adding new code, we can perform the experiment to verify what happens if we move the pushable item over the road of the robot.
In above picture you see that the robot will ignore fully the presence of the pushable item, and it will pass throught it.

We could add a code to detect the presence of the pushable object and when it is present, change direction to avoid that sector like it was a wall.
Oddly, we'll not type this code in Collision() management procedure of the robot, because that procedure is ONLY for lara and it will be neither called when lara is far from the robot.
We'll type the code in IsFreeWay() function, that it will be called from Control() managmement procedure.
The change in IsFreeWay() function it will be to consider also the presence of pushable items like an obstacle.

          // no obstacles about geometry of room, now we'll check if there is a pushable item in the next sector
          MyPos.CordX = NewX;
          MyPos.CordY = NewY;
          MyPos.CordZ = NewZ;

          Find(enumFIND.ITEMS_SECTOR, -1, FLOOR.RoomIndex, -1, 0, &MyPos);

          // now we check if one of these items is a pushable item
          // note: we have to save the current GET.pItem structure (that is the cleaner robot) before
          // using Get(enumGET.ITEM) that will change that structure
          // at end we'll restore the old value for GET.pItem
          pSaveItem = GET.pItem;

          for (i=0;i<FIND.TotItems;i++) {
                    Index = FIND.VetItems[i];

                    Get(enumGET.ITEM, Index,0);

                    if (GET.pItem->SlotID >= enumSLOT.PUSHABLE_OBJECT1 && GET.pItem->SlotID <= enumSLOT.PUSHABLE_OBJECT5) {
                              // one of the item in that sector is own a pushable object: return false because the way is not free
                              // retore previous structure in GET.pItem
                              GET.pItem = pSaveItem;
                              return false;
                    }
          }
          // retore previous structure in GET.pItem
          GET.pItem = pSaveItem;          
          return true;

}}

Where we completed computes about collision with room geometry and (in old code) we set that the way is free, now we check also for the presence of a pushable item in the (next) sector we are studying.
Please, note that it has been necessary saving and then restoring the structure GET.pItem because we changed it, after having called the Find() function, to verify the slot type of found item.
This operation to save and restore (or simply duplicate) a global strutcture in some local variable of same type, it could happen very often. Everytime you call newly Get() or Find() function but you wish preserve some value of previous call, you should save the wished sub structure in some local variable of same type.
For instance we had to declare this local variable in IsFreeWay() function to save (and then restore) the GET.pItem structure:

          StrTriplePoint MyPos;          
          int i;
          int Index;
          StrItemTr4 *pSaveItem;

We had to declare other local variable to check the found item in the vector FIND.VetItems[] (the i variable), the MyPos structure variable to save the 3d point and pass its pointer to Find() function, and the Index variable to host the current index of item we are verifying.

Now we build the project and verify what happens in game...




Ok it works.
In this way we could use same trick used in tomb raider 3 to change the direction of the cleaner robot, moving a pushable object.


How to do Robot killed enemies

When we build a new object from scratch, we have to manage all situations about collisions, since there is no automatic management for new objects.
We chose to consider a pushable object an obstacle like a wall, to do turn robot in another direction.
Now we can set what it will happen when the robot collides with some creature.
For instance we can kill all creatures those collided with the cleaner robot.
We'll place this new code at end of ControlRobotCleaner() function

          // check if the robot is colliding with some creature
          // save the strucure item of the robot in local variable "pRobot"
          pRobot = GET.pItem;

          Find(enumFIND.ITEMS_NEARBY, -1, pRobot->Room, -1, 1024, &pRobot->CordX);

          for (i=0;i<FIND.TotItems;i++) {
                    Index= FIND.VetItems[i];

                    // ignore if it is own the same robot cleaner
                    if (Index != ItemIndex) {
                              // there is a moveable item very closed (max one sector of distance) to the robot (and it is not the same robot)
                              // get the structure of this found item
                              Get(enumGET.ITEM, Index,0);
                              // verify that it was a creature and it has been enabled
                              if (TriggerActive(GET.pItem) == true && (GET.pItem->FlagsMain & enumFITEM.CREATURE) != 0) {
                                        // it has been enabled and it is a creature
                                        // now verify if the bounding box is colliding
                                        if (TestBoundCollide(GET.pItem, pRobot, 512)==true) {

                                                  // there is a collision: kill the enemy
                                                  // if it is a semigod try to kill with explosion
                                                  // discover if it is a semigod
                                                  Get(enumGET.SLOT, GET.pItem->SlotID,0);
                                                  
                                                  if (GET.pSlot->Flags & enumFSLOT.NO_DAMAGE_FOR_NO_EXPLOSIVE_AMMO) {
                                                            // unkillable (with common measures) try to kill it with an explosion
                                                            PerformActionTrigger(NULL, 14, Index, 2);
                                                            // and then remove it (for those hard to die)
                                                            PerformActionTrigger(NULL, 14, Index, 6);
                                                  }else {
                                                            // it's a common mortal: clear its health
                                                            GET.pItem->Health=0;
                                                  }
                                        }
                              }

                    }
          }


In new adding of code we required also to declare some local variable (at top) of ControlRobotCleaner() function

          StrItemTr4 *pRobot;
          int i;
          int Index;


Robot cleaner: a kill machine



After last changes the robot will kill any creature it meets on its way. (Note: to trigger the enemies you find a blue sector at opposide side of the room)
In above picture we see the SAS (a picture) and in B and C the killing of skeleton.
To kill semigod (it's not the case of skeleton, but it could happens to collide with seth or others), there are two attempts to kill it: First with an explosion:

          // unkillable (with common measures) try to kill it with an explosion
          PerformActionTrigger(NULL, 14, Index, 2);

Above action trigger has been gotten with [Export Function] button of Set Trigger Type window of NGLE.
We got this export report:

; Set Trigger Type - ACTION 14
; Exporting: TRIGGER(1038:0) for ACTION(64) {Tomb_NextGeneration}
; <#> : ANIMATING1 ID 64 in sector (2,4) of Room6
; <&> : Enemy. Kill <#>object in (E) way
; (E) : Exploding creature

PerformActionTrigger(NULL, 14, 64 | NGLE_INDEX, 4);

Then we replace the index (ngle) supplied by export report, with the index of item we found with Find() function.
Since some semigod cann't be killed neither with an explosion, we applied to it also another action trigger to remove the enemy in any circustance:


          // and then remove it (for those hard to die)
          PerformActionTrigger(NULL, 14, Index, 6);

The export report for this action trigger was:

; Set Trigger Type - ACTION 14
; Exporting: TRIGGER(1550:0) for ACTION(-1) {Tomb_NextGeneration}
; <#> :
; <&> : Enemy. Kill <#>object in (E) way
; (E) : Hide Creature (when other ways don't work)

PerformActionTrigger(NULL, 14, -1 | NGLE_INDEX, 6);


How to turn gradually the direction

In Exercise 2 there was the Homework 1 about turning gradually the cube when it changes its direction of 90 degrees.
Now we'll do that job for our cleaner robot.
We need some global variable to host two values:

Fortunately we have yet some free variables in item structure of our robot, so we'll use:

          short Reserved_38;                                        
          short Reserved_3A;                    

We'll use the variable "Reserved_38" to host the state id of the robot. For instance we can assign "0" for "common moving forward", and "1" for "it's turning on its axis"
While in "Reserved_3A" we'll type the signed increment to change horizontal facing.
About the final orienting we can discover it checking the singed increment and store dynamically it using a local variable.
Now we have to initialise the two new global variables, and we'll this in InitialiseRobotCleaner() function of our robot:

          GET.pItem->Reserved_38 = 0; // state id "0"= moving forward / "1"= turning of 90 degreess
          GET.pItem->Reserved_3A = 0; // signed increment to change current horizontal facing to final facing
}

Now we change the code in ControlRobotCleaner(), where we are checking for new directions.
We'll change this old code:

          // it the (relative) direction is different than 0 (0=continue forward), we'll have to change also
          // the facing of the robot
          if (Direction != 0) {
                    GET.pItem->OrientationH += Direction;
          }

In above (old) code the "Direction" variable contain the new "relative" direction. It means that, if it is "0" there is no change of direction, while if it's different the robot will have to turn in that direction.
Now we change above code in this way:

          // it the (relative) direction is different than 0 (0=continue forward), we'll have to change also
          // the facing of the robot
          if (Direction != 0) {
                    // we set the new state id to turn the robot of wished angle
                    GET.pItem->Reserved_38 = 1;
                    // and we type the signed increment to use to change from current facing to final facing
                    if (Direction > 0) {
                              // moving in clockwise turning:
                              GET.pItem->Reserved_3A = 256;
                    }else {
                              // moving inverse clockwise
                              GET.pItem->Reserved_3A = -256;
                    }
                    // perform immedialty first change
                    GET.pItem->OrientationH += GET.pItem->Reserved_3A;

          }

But we have to do some other change...
In following code we have to avoid any movement in the space or change about vertical orienting, since now (when the Reserved_38 == 1) , the robot has only to turn on its axis.
So we'll perform some conditions to avoid changes when Reserve_38 == 1.

But now it's better restyle our ControlRobotCleaner() function, because it's becoming a bit messed up.

Restyle the ControlRobotCleaner() function creating sub-functions

We could create new litte functions, one for each basic management: Move the robot, Turn the robot, check collision of robot (with enemies or pushable objects) ect.
In spite it was a bit boring this restyling it is necessary to keep well sorted the main code of the cleaner robot.

Manage the horizontal turning: the RobotCleanerHTurning() function

When the special state id (stored in Reserved_38 variable) is "1", the robot is turning on its axis and it will be called this function:

// already set values in GET.pItem (robot cleaner item structure)
void RobotCleanerHTurning(void)
{
          short Facing;
          // the robot is only turning on itself:
          // verify if the turning has been completed
          // it happens when the robot is perfectly facing to some hortogonal direction
          Facing= GET.pItem->OrientationH;
          if (Facing == enumORIENT.EAST || Facing == enumORIENT.NORTH ||
                    Facing == enumORIENT.SOUTH || Facing == enumORIENT.WEST) {
                    // reached the final facing.
                    // come back to moving phase
                    GET.pItem->Reserved_38 =0;
                    return;
          }
          // we are yet in turning phase:
          // change the current facing of the robot
          GET.pItem->OrientationH += GET.pItem->Reserved_3A;
}


Manage the movement and check for free direction: the RobotCheckDirections() function

When the special state-id of robot (stored in Reserved_38 variable) is "0", the robot is moving and it will be necessary check for free way.
In this situation we'll call this function:

// in GET.pItem there is robot cleaner item structure
// this function will return "false" if there is no direction and the robot has been destroyed
bool RobotCheckDirections(int ItemIndex)
{
          bool TestOk;
          short Direction;
          int TopSideY;

          // we are in "moving" phase
          Direction = 0;
          TestOk=false;
          TopSideY= GET.pItem->CordY - 1023;

          // try to continue to move forward
          if (IsFreeWay(ItemIndex, Direction, GET.pItem->Reserved_36, 511, TopSideY ) == true) {
                    // it is possible continue to move forward
                    TestOk=true;
          }
          if (TestOk==false) {          
                    // it was not possible to move forward: try to turn at left
                    Direction= -16384; // (- 0x4000 90 degrees at west);
                    if (IsFreeWay(ItemIndex, Direction, GET.pItem->Reserved_36, 511, TopSideY ) == true) {
                              // it is possible turn at left
                              TestOk=true;
                    }
          }

          if (TestOk==false) {
                    // it was not possible move forward and neither turning at left: try to turn at right
                    Direction = 16384;
                    if (IsFreeWay(ItemIndex, Direction, GET.pItem->Reserved_36, 511, TopSideY )==true) {
                              // it is possible turn at right
                              TestOk=true;
                    }
          }
          if (TestOk == false) {
                    // it has been NOT possible move in any direction: to do explode the cube
                    PerformActionTrigger(NULL, 14, ItemIndex, 2);

                    return false;
          }
          // it's possible to do move the robot
          // it the (relative) direction is different than 0 (0=continue forward), we'll have to change also
          // the facing of the robot
          if (Direction != 0) {
                    // we set the new state id to turn the robot of wished angle
                    GET.pItem->Reserved_38 = 1;
                    // and we type the signed increment to use to change from current facing to final facing
                    if (Direction > 0) {
                              // moving in clockwise turning:
                              GET.pItem->Reserved_3A = 256;
                    }else {
                              // moving inverse clockwise
                              GET.pItem->Reserved_3A = -256;
                    }
                    // perform immedialty first change

                    GET.pItem->OrientationH += GET.pItem->Reserved_3A;
          }
          return true;
}

In above function we set as input parameter the index of the robot, and it will return "true" or "false" to inform if it has been found a direction or less.
When it will return "false" it means that there was no direction and the robot has been done exploding.

Chaning position of the robot and check for the vertical turning: the RobotMoveAndVturning() function

If the state id is for moving, the position of the robot will be changed and there will the check for the slope of next sector.

void RobotMoveAndVturning(int ItemIndex)
{

          short VerticalOrientWished;
          short VerticalOrientInc;
          int IncX;
          int IncZ;

          // change vertical orient only if the robot is in "moving" status (not the turning status)
          if (FLOOR.SlopeType == enumSLOPE.FLAT) {
                    // next sector is FLAT, set wished vertical orienting as 0x0000
                    VerticalOrientWished = 0;
          }else {
                    // next sector is NOT flat, so it will be a slope.
                    // we discover if it is a rise or a declivity
                    // if the direction of the slope is the same of that of our object, then it is a "rise"
                    if (FLOOR.SlopeOrienting == GET.pItem->OrientationH) {
                              // it is a rise
                              // set wished value for rise
                              VerticalOrientWished = 0x0999;
                    }else {
                              // otherwise it will be a declivity
                              VerticalOrientWished = -0x999;
                    }
          }

          // if the two vertical orienting, current and next, are the same, we have not to change anything
          // and so we'll set as 0 the increment to change vertical orienting
          if (GET.pItem->OrientationV == VerticalOrientWished) {
                    VerticalOrientInc = 0;
          }

          if (GET.pItem->OrientationV > VerticalOrientWished) {
                    // current vertical orienting is greaten than final: we have to use a negative increment to reach it
                    VerticalOrientInc = -GET.pItem->Reserved_34;
          }

          if (GET.pItem->OrientationV < VerticalOrientWished) {
                    // current vertical orienting if less than final: we have to use a positive increment to reach it
                    VerticalOrientInc = GET.pItem->Reserved_34;
          }

          // change the vertical orienting
          GET.pItem->OrientationV += VerticalOrientInc;

          // now compute the new position of the item using its facing because we have already changed it if it was
          // necessary
          GetIncrements(GET.pItem->OrientationH, &IncX, &IncZ, GET.pItem->Reserved_36 );

          GET.pItem->CordX += IncX;
          GET.pItem->CordZ += IncZ;
          // in the case the cube was over a slope we have to change also its Y coordinate, setting it to same
          // value of floor height
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          GET.pItem->CordY = FLOOR.FloorHeight;

          // we have to update room of object in the case it was changed
          UpdateItemRoom(ItemIndex);
}

Also above function requires the index of the robot as input argument.

Adding special effects to the robot: the RobotAddEffects() function


void RobotAddEffects(int ItemIndex)
{

          int Value;

          Value= GetRandomControl() & 7;

          if (Value ==0) {
                    
                    AddSparksToCables(ItemIndex, 5);
                    AddSparksToCables(ItemIndex, 9);
                    AddSparksToCables(ItemIndex, 13);
          }
}


Detect collision with enemies: the RobotCheckEnemyCollisions() function


void RobotCheckEnemyCollisions(short ItemIndex)
{
          StrItemTr4 *pRobot;
          int i;
          int Index;

          // check if the robot is colliding with some creature
          // save the strucure item of the robot in local variable "pRobot"
          pRobot = GET.pItem;

          Find(enumFIND.ITEMS_NEARBY, -1, pRobot->Room, -1, 1024, &pRobot->CordX);

          for (i=0;i<FIND.TotItems;i++) {
                    Index= FIND.VetItems[i];

                    // ignore if it is own the same robot cleaner
                    if (Index != ItemIndex) {
                              // there is a moveable item very closed (max one sector of distance) to the robot (and it is not the same robot)
                              // get the structure of this found item
                              Get(enumGET.ITEM, Index,0);
                              // verify that it was a creature and it has been enabled
                              if (TriggerActive(GET.pItem) == true && (GET.pItem->FlagsMain & enumFITEM.CREATURE) != 0) {
                                        // it has been enabled and it is a creature
                                        // now verify if the bounding box is colliding
                                        if (TestBoundCollide(GET.pItem, pRobot, 512)==true) {

                                                  // there is a collision: kill the enemy
                                                  // if it is a semigod try to kill with explosion
                                                  // discover if it is a semigod
                                                  Get(enumGET.SLOT, GET.pItem->SlotID,0);
                                                  
                                                  if (GET.pSlot->Flags & enumFSLOT.NO_DAMAGE_FOR_NO_EXPLOSIVE_AMMO) {
                                                            // unkillable (with common measures) try to kill it with an explosion
                                                            PerformActionTrigger(NULL, 14, Index, 2);
                                                            // and then remove it (for those hard to die)
                                                            PerformActionTrigger(NULL, 14, Index, 6);
                                                  }else {
                                                            // it's a common mortal: clear its health
                                                            GET.pItem->Health=0;
                                                  }
                                        }
                              }

                    }
          }

}


The new layout of ControlRobotCleaner() function

Now we can resyle the ControlRobotCleaner() function, calling the above functions in according with the state id of the robot.

void ControlRobotCleaner(short ItemIndex)
{

          Get(enumGET.ITEM, ItemIndex,0);

          if (TriggerActive(GET.pItem)==false) return;
          // animate the robot (turning the cables)
          AnimateItem(GET.pItem);

          Get(enumGET.LARA,0,0);
          if (GET.pLara->Health < 1) {
                    // lara is dead, stop movements of the robot
                    return;
          }
          if (GET.pItem->Reserved_38 == 1) {
                    RobotCleanerHTurning();
          }

          // if we are yet in turning phase we have to skip all code to detect new directions:
          if (GET.pItem->Reserved_38 == 0) {

                    if (RobotCheckDirections(ItemIndex)==false) return;
          }
          // check newly if it has been just changed the state id "moving/turning":
          if (GET.pItem->Reserved_38 == 0) {

                    RobotMoveAndVturning(ItemIndex);
          }
          RobotAddEffects(ItemIndex);
          RobotCheckEnemyCollisions(ItemIndex);
}


Final testing of Cleaner Robot



The new code to turn gradually the horizontal direction works fine.
For our didactic targets the exercise is complete.


Homeworks
As homeworks you could try to improve the code of Robot Cleaner in following ways:

  1. In IsFreeWay() function you could add a check also for other moveables in next sector, as we did for the pushable objects.
    For instance it could be useful detecting the presence of other Cleaner robot and verify if with two robots in same wide room, they are able to avoid to collide changing direction in right moment.

  2. Add hardcoded sound of playeffect using the tomb4 function:
    SoundEffect(int NumeroSample, void *pCoordinate, int Flags);
    Note: the *pCoordinate is the point of X,Y,Z position of the sound, and you can pass the coordinate of robot in this way:
    SoundEffect(N, &GET.pItem->CordX, 0);
    Try to add different sounds in according with current movement: a sound when the robot is turning and another sound when it is moving forward.





Exercise 4: Star Wars Robot

This exercise begins in room 13 of plugins project


In this exercise we'll build another object, again a robot, but this time we'll concentrate our attention about new issues.
Our new robot is more advanced: it will be able to detect the presence of Lara and shot her with electrical lightning.
We'll see how to pass settings via OCB and with our customize script commands.
About moving of the robot, we'll discover also how detect static items and avoid or shatter them.
While for common movements in the level we'll use the code we did for cleaner robot with only few changes.
One change it's to handle the different size of star wars robot respect of cleaner robot. (See picture at left)
Since the SW robot is littler than cleaner robot, there is a problem about alignment at center of the sectors.
When we check to discover if robot boundary is going on sector with some obstactle, the robot will be not at middle of current sector but more closed to its boundary, becasue it is littler and its half/width is not 512 but less.
For this reason it could happen that at first change of direction the SW Robot will be misaligned with sector middle-line.
To avoid this situation we have to perform the check free way in next sector only when the SW Robot is at center of its current sector, and we'll use an half/width distance like it was yet 512.
Pratically the real difference respect to the cleaner robot code, is that now we'll perform the control for freeway only once for sector, and only when the SW robot is at center of current sector, while with other robot we did this check after every micro movement.


Set basic code for Star Wars Robot

Since we have already described the code for new object in Exercise 3: The Cleaner Robot now I show the basic code skipping comments, then we'll see better only the news for SW Robot.

Code to initialise slot of Star Wars Robot
In the callback cbInitObjects() we have to do same job we did for Cleaner Robot, anyway now it's better change a bit the layout of our code because in our previous release we worked like that only our object was the Cleaner Robot while we could have many new object.
So that side of code where we typed:

void cbInitObjects(void)
{
          Get(enumGET.SLOT, enumSLOT.ROBOT_CLEANER, 0);
          if ((GET.pSlot->Flags & enumFSLOT.PRESENT) == 0) {
                    // the flag PRESENT is missing, so there is NO robot cleaner in this level: we quit immediatly the code
                    return;
          }

It's not good, because if we quit the cbInitObjects() function when the ROBOT_CLEANER is missing, then we cann't check if other our objects, like the ROBOT_STAR_WARS, is present to initialise them.
So it's better creating a single little function to check and initialise the robot cleaner, and another to do same work for star wars robot, and all these function should be called from cbInitObjects() callback.
So we create a function to manage the slot of robot cleaner:

void InitSlotRobotCleaner(void)
{
          Get(enumGET.SLOT, enumSLOT.ROBOT_CLEANER, 0);
          if ((GET.pSlot->Flags & enumFSLOT.PRESENT) == 0) {
                    // the flag PRESENT is missing, so there is NO robot cleaner in this level: we quit immediatly the code
                    return;
          }

          GET.pSlot->pProcInitialise = InitialiseRobotCleaner;
          GET.pSlot->pProcCollision = CollisionRobotCleaner;
          GET.pSlot->pProcControl = ControlRobotCleaner;

          GET.pSlot->Flags = enumFSLOT.AFFECT_LARA_AT_CONTACT |
                                                            enumFSLOT.CHANGE_POS_ITEM |
                                                            enumFSLOT.NO_DAMAGE_FOR_NO_EXPLOSIVE_AMMO |
                                                            enumFSLOT.SAVE_ALL_DATA |
                                                            enumFSLOT.SFX_LOCAL_SOUND |
                                                            enumFSLOT.USE_COLLISION_BOX;
}

Then another function with same target (and in this case, with almost same code) for the star wars robot:

void InitSlotRobotStarWars(void)
{
          Get(enumGET.SLOT, enumSLOT.ROBOT_STAR_WARS, 0);
          if ((GET.pSlot->Flags & enumFSLOT.PRESENT) == 0) {
                    // the flag PRESENT is missing, so there is NO star wars robot in this level: we quit immediatly the code
                    return;
          }

          GET.pSlot->pProcInitialise = InitialiseRobotStarWars;
          GET.pSlot->pProcCollision = CollisionRobotStarWars;
          GET.pSlot->pProcControl = ControlRobotStarWars;

          GET.pSlot->Flags = enumFSLOT.AFFECT_LARA_AT_CONTACT |
                                                            enumFSLOT.CHANGE_POS_ITEM |
                                                            enumFSLOT.NO_DAMAGE_FOR_NO_EXPLOSIVE_AMMO |
                                                            enumFSLOT.SAVE_ALL_DATA |
                                                            enumFSLOT.SFX_LOCAL_SOUND |
                                                            enumFSLOT.USE_COLLISION_BOX;
}

And now from the cbInitObjects() callback, we call both above function, in this way also the missing of one of them will not forbid to others to be initialised:


void cbInitObjects(void)
{

          InitSlotRobotCleaner();
          InitSlotRobotStarWars();

}

Now we have to create the three management procedure for star wars robot:
InitialiseRobotStarWars();
CollisionRobotStarWars();
ControlRobotStarWars();
Since the most code is the same of the cleaner robot it's more easy if we copy and past that code and then we'll change only the names of those three procedures and we'll perform some other change where it will be necessary.


The InitialiseRobotStarWars() function
So we copy the InitialiseRobotCleaner() function, paste them in other side of the source, and rename it with new name: InitialiseRobotStarWars:

void InitialiseRobotStarWars(short ItemIndex)
{
          Get(enumGET.ITEM, ItemIndex,0);

          GET.pItem->StateIdCurrent = 0;
          GET.pItem->StateIdNext = 0;

          Get(enumGET.SLOT, enumSLOT.ROBOT_STAR_WARS,0);
          
          GET.pItem->AnimationNow = GET.pSlot->IndexFirstAnim;

          Get(enumGET.ANIMATION, GET.pItem->AnimationNow,0);

          GET.pItem->FrameNow = GET.pAnimation->FrameStart;

          GET.pItem->FlagsMain = enumFITEM.NOT_YET_ENABLED | enumFITEM.CREATURE | enumFITEM.NOT_VISIBLE;

          // initialise horizontal speed
          GET.pItem->Reserved_36 = 32;
}

If you compare above function with that (original) of cleaner robot, you discover that we made also following changes:



The CollisionRobotStarWars() function
Now we make the same job also for collision() procedure. We copy the CollisionRobotCleaner() code, and paste them in other side of the source and then we rename it as "CollisionRobotStarWars":

void CollisionRobotStarWars(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision)
{
          Get(enumGET.ITEM, ItemIndex, 0);

          if (TriggerActive(GET.pItem)== false) return;

          if (GetMaxDistance(&GET.pItem->CordX, &pLara->CordX, false) > 1024) return;

          if (TestBoundCollide(GET.pItem, pLara, pLaraCollision->LaraSizeX)==false) return;

          if (TestCollision(GET.pItem, pLara)==false) return;

          // lara and robot are in collision
          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);
}

Also in above code we did some change.
We removed all code about killing Lara and also the check for collision with forward side of the robot, because our new robot has not electric cables in front.
About the chance to kill lara at contact we'll add it in next time, because we'll let to level builder the chance to enable or less this feature whereby OCB setting.


The ControlRobotStarWars() function
In the Control() procedure we'll have most changes because the working mode of new robot is enough different of cleaner robot.
Anyway we can copy at least the basic introduction of control procedure, that is the same for all objects:

void ControlRobotStarWars(short ItemIndex)
{
          Get(enumGET.ITEM, ItemIndex,0);

          if (TriggerActive(GET.pItem)==false) return;
}

Then we'll have to add all code to manage the AI of star wars robot but before beginning it's necessary knowing better our robot, its animations and state-ids.
Differently by cleaner robot, where there were only two state-ids for moving forward "0", and turning on itself "1", saved in a reserved variable, with the star wars robot we'll have many state-id and we'll keep them own in "StateIdCurrent" and "StateIdNext" variables of item structure.

Animations of Star Wars Robot

When we build a new object from scratch we should begin building new animations for it and choose the wished set of abilities about movements that it will be able to do.
As general rule, we'll have a most common animation that it will work like a standard animation, like for Lara it was the stand-up animation.
Other animation will begin a new action but then we'll need of another animation to come back to standard animation our object.
Now we see the set of animations for our Start Wars Robot...

Animations of Star Wars Robot

Anim Next State Description
0 0 0 Flat feet, down fixed head
The robot moves forward
1 2 1 From flat feet to rising, head down fixed forward
2 2 1 Feet for fix rising. Fixed down forward Head
3 4 1 Feet from flat to declivity. Fixed down forward Head
4 4 1 Fixed declivity feet. Fixed down forward Head
5 0 1 Feet from rising to flat. Fixed down forward Head
6 0 1 Feet from declivity to flat. Fixed down forward head.
7 0 2 Lower head moves at left and back to forward. Fixed flat feet. No moving forward
8 0 2 Lower head moves at right and back to forward. Fixed flat feet. Do not move forward
9 10 3 Move up the head to reach middle height.
10 10 4 Fixed middle height for head
11 0 3 Move down head, from middle height to standard
12 13 3 Moving up head, from standard to super high
13 13 4 Fixed highest head
14 13 2 Highest head, turning at left and back to forward
15 13 2 Highest head, turning at right and back to forward
16 0 5 Steer at left by 90 degrees
17 0 5 Steer at right by 90 degrees
18 0 5 Steer by 180 degrees to reverse direction
19 19 6 Not move, no change feet, no turn head. In this animation it is like an animating. To use to manage the robot avoiding interference with AnimateItem() tomb function
20 10 2 Middle height head, turn at left and come back
21 10 2 Middle heigth head turns at right and the come back
22 0 3 Move down head, from highest position to standard


The abilitites of Star Wars Robot

Looking previous table and comparing it with animation editor program to watch robot's animations, you can discover what are the skills about movements of our robot.
It is able to turn in vertical way its feet to move over one click slopes (declivity or rising).
It is able to turn the head at left and right to look around and discover its target (lara)
It is able to move up/down its head, and there are two different heights: middle height and super hight.
Also while its head is super high (or in standard height) it is able to turn it at left or right to look around.

AI behaviour of StarWars Robot

Standard behaviour of SW robot should be the following:

  1. It moves with robotic standard movements, changing direction when it meets an obstacle

  2. When it has been enabled the "supervisory control" mode whereby OCB, it will move the head at left and rigth also to check directions where it didn't mean move but that it has to control.

  3. When, in it's field of view, it enters Lara and it had "supervisory control" target, it will try to follow, or shot lara, or simply to trigger something to cry havoc. Above reactions will be affected by OCB and/or script settings.

  4. When it has at its left or right low walls (about one sector of height) it will move up the head at super high and look over these walls, turning the head as necessary to detect Lara's presence.

  5. In the case it has been set to shoot lara, it will do a short preparation of shooting to give a chance to Lara (and to the player) to avoid the shooting, since the lightning should be too fast to be avoided in other manners.


Meaning of State-IDs

We already knew the state-ids, anyway now, when we have to build code to animate our new object, it's necessary we understood better their meaning.
A state id is a value that we save in item structure to REMEMBER what our object was doing in previous frame.
Since our code it will be called only once for frame it should be stressing to have to compute newly all possible chances about what is doing our object, while, thanks to state id values, we can remember that it was "moving", "aimining", "turning" ect. And when our code will get newly the control, we'll, as first point, read the state id value to remember what micro action it was doing to get easy our target to discover what we have to do now to manage current situation.
About this speech is important understanding the meaning for each state id value we set for animations of our robot.

State IDs of Star Wars Robot

State Description
0 Moving forward with down forward head and flat feet
1 Robot is moving forward, turning its feet to change from slope to flat floor or viceversa. In this phase it cann't turn on itself and neither shooting or looking around moving the head.
Anyway also in this situation it will be able to detect lara but only if she is in front of it, since it is not able to turn the head to control other directions
2 It is moving its head at left or right to detect Lara. In this state id it's not able to move forward.
3 It is moving up or down the head. In this phase it is not able to move, to shoot or to change direction turning on itself, but it could detect lara if she is where it is looking, in front itself
4 It is fixed with head in high position and in this state it could shoot lara if she had been detected just a moment before of this state.
5 The robot is turning on itself to change direction. The change could be by 90 or 180 degrees. In this state-id it could detect lara only when she is in front of it
6 In this state-id the robot is fully frozen, no speed, no change about feet or head. This state it will be used to move manually it avoiding interference with AnimateItem() function



Moving the Star Wars Robot

Looking above animation and state-id list we discover some difference also about the moving of SW robot respect the cleaner robot.
Now, with SW Robot we'll use "active" animations, where for "active" we mean that there is a speed value.
When an animation is active the AnimateItem() tomb function will manage byself the moving of the object, anyway this self management doesn't affect the check for collisions, so we'll have to use yet the IsFreeWay() function to detect when to change direction.
Other active animationa are the 16, 17 and 18 animations.
In this case it's not the speed (set to 0) but the rotation of the object to get "active" these animations.
When we wish that the robot turn at left, for instance, we'll set the animation 16, and AnimateItem() will turn the robot on its axis in inverse clockwise until reach 90 degrees and then (for next animation field) always the AnimateItem() will set new animation 0 to move newly forward.
Another difference is that the SW robot will not explode for missing free ways.
When it is not able to move forward, at left and at right, it will inverse by 180 degrees the direction (using animation 18) and it will come back from where it came.
Remembering all this speech now we see the first release of ControlRobotStarWars() procedure to do move the robot.

void ControlRobotStarWars(short ItemIndex)
{

          bool TestOk;
          int TopSideY;

          Get(enumGET.ITEM, ItemIndex,0);

          if (TriggerActive(GET.pItem)==false) return;

          if (GET.pItem->SpeedH > 0) {
                    // the robot is moving: check for freeway and collisions

                    // discover infos about floor of current position of robot
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);

                    // update Y coordinate and room of robot
                    GET.pItem->CordY = FLOOR.FloorHeight;

                    // we have to update room of object in the case it was changed
                    UpdateItemRoom(ItemIndex);

                    if (GetDistanceXZ(GET.pItem->CordX, GET.pItem->CordZ,
                                                                      FLOOR.SectorCoords.MiddleX, FLOOR.SectorCoords.MiddleZ) < 16) {
                              // the robot is at middle of current sector.
                              // verify collision in next sector only now (when the robot is at middle of current sector)
                              
                              TopSideY= GET.pItem->CordY - 1000;
                              // try to move forward:
                              TestOk = IsFreeWay(ItemIndex, 0, GET.pItem->SpeedH, 511, TopSideY);

                              if (TestOk==false) {
                                        // try to turn at left (at its west)
                                        TestOk = IsFreeWay(ItemIndex, -16384 , GET.pItem->SpeedH, 511, TopSideY);
                                        if (TestOk==true) {
                                                  // ok: free way at west: set animation to turn at left of 90 degrees
                                                  ForceAnimationForItem(GET.pItem, 16, -1);                                                  
                                        }
                              }                    

                              if (TestOk==false) {
                                        // try to turn at right (at its east)
                                        TestOk = IsFreeWay(ItemIndex, 16384 , GET.pItem->SpeedH, 511, TopSideY);
                                        if (TestOk == true) {
                                                  ForceAnimationForItem(GET.pItem, 17, -1);
                                        }
                              }

                              if (TestOk==false) {
                                        // try to come back, it should be always possible since we are coming from that direction
                                        TestOk=true;
                                        ForceAnimationForItem(GET.pItem, 18, -1);
                              }
                    }
          }
          // animate the robot
          AnimateItem(GET.pItem);
}



Short description of first release of Control() function
Speaking only about the news, we see that we performed a first test about current speed of the robot:

          if (GET.pItem->SpeedH > 0) {
                    // the robot is moving: check for freeway and collisions

                    // discover infos about floor of current position of robot
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);

                    // update Y coordinate and room of robot
                    GET.pItem->CordY = FLOOR.FloorHeight;

                    // we have to update room of object in the case it was changed
                    UpdateItemRoom(ItemIndex);

We'll check the collisions with room geometry only when the robot is moving and we discover this situation testing its SpeedH field.
Then we have gotten infos about the floor in current position of the robot to update its Y coordinate, since this job it will be not done by the AnimateItem() function.
We had also to update the room of moveable, since that (oddly) neither this job it will be performed from AnimateItem() function.

To discover when the robot is at center of the sector we used the floor infos about the coordinate of middle point of current sector and then we compared them with coordinates of the robot to verify if it is very closed to the middle, with a max distance lower than current speed (32 game units):

                    if (GetDistanceXZ(GET.pItem->CordX, GET.pItem->CordZ,
                                                                      FLOOR.SectorCoords.MiddleX, FLOOR.SectorCoords.MiddleZ) < 16) {
                              // the robot is at middle of current sector.
                              // verify collision in next sector only now (when the robot is at middle of current sector)                              


About the check about freeway, the code is very alike than that of cleaner robot, anyway in this case the difference is about the mode we used to to rotate the robot.
Now we'll set simply the right "active" animation to do turn the robot to wished direction:

                              TopSideY= GET.pItem->CordY - 1000;
                              // try to move forward:
                              TestOk = IsFreeWay(ItemIndex, 0, GET.pItem->SpeedH, 511, TopSideY);

                              if (TestOk==false) {
                                        // try to turn at left (at its west)
                                        TestOk = IsFreeWay(ItemIndex, -16384 , GET.pItem->SpeedH, 511, TopSideY);
                                        if (TestOk==true) {
                                                  // ok: free way at west: set animation to turn at left of 90 degrees
                                                  ForceAnimationForItem(GET.pItem, 16, -1);                                                  
                                        }
                              }                    

                              if (TestOk==false) {
                                        // try to turn at right (at its east)
                                        TestOk = IsFreeWay(ItemIndex, 16384 , GET.pItem->SpeedH, 511, TopSideY);
                                        if (TestOk == true) {
                                                  ForceAnimationForItem(GET.pItem, 17, -1);
                                        }
                              }

                              if (TestOk==false) {
                                        // try to come back, it should be always possible since we are coming from that direction
                                        TestOk=true;
                                        ForceAnimationForItem(GET.pItem, 18, -1);
                              }

We used the ForceAnimationForItem() function to set the wished animaton. We supplied also "-1" as "NextStateId" argument, because we wish use the next state id field of given animation record.

At end of control() function we called the AnimateItem() function:

          // animate the robot
          AnimateItem(GET.pItem);
}

It was necessary call AnimateItem() function AFTER the control about collision, because it will be the AnimateItem() function to do move the robot but we have to check in advance if in current direction (facing, "OrientingH" field) the way is free.

Now we build the plugin and try in game what happens...


Turn the feet on the slope



First experiment is not so bad.
The robot avoid walls, and turn on itself before changing direction as we hoped.
Anyway there is some problem to fix, of course.
Look above picture: the robot when move over the rising it doesn't change inclination of its feet.
Rather of a bug it is a missing: we have not yet typed the code to manage this situation.
We have different animation to do turn in vertical way the feet in according with change of slope/flat status of floor.
We can read newly from animation table these animations:

Anim          Next          State-Id Description
------------------------------------------------------------------------------------------------------
0            0            0            Flat feet, down fixed head
1            2            1            From flat feet to rising, head down fixed forward
2            2            1            Feet for fix rising. Fixed down forward Head
3            4            1            Feet from flat to declivity. Fixed down forward Head
4            4            1            Fixed declivity feet. Fixed down forward Head
5            0            1            Feet from rising to flat. Fixed down forward Head
6            0            1            Feet from declivity to flat. Fixed down forward head.
------------------------------------------------------------------------------------------------------

To choose the right number of animation to set, we need of two values: the inclination of current sector (where the robot stands) and the inclination of next sector, where the robot is going to go.
When we'll have these two values, we'll be able, with some comparisons, to discover the right animation number to set.
Since we have to remember not only if a sector is flat or less, but also (when it is not flat) the type of slope (rising or declivity) we'll need of some local variables to store these values.
We'll use the "SlopeOrienting" variable of FLOOR structure to know what is the direction of the rising and then, comparing this facing with that of the robot we'll convert these data in a value like Rising/Declivity respect to movement of the robot.
We'll use two local variables:

          int FirstSectorType;
          int NextSectorType;

Both variable could host value to remember following thee situations:

  1. Sector is flat

  2. Sector is a rising

  3. Sector is a declivity

When we need to identify a situation (like sector type) with a number, it's better creating some mnemonic constant to get more readable the code.
So we go to "constants_mine.h" source, and we'll create these constant names:

#define SECTOR_FLAT                    0
#define SECTOR_RISING                    1
#define SECTOR_DECLIVITY          2



Code to discover the sector type in according with direction of the robot

Now we have to type the code to discover what sector type is that sector (with data in FLOOR structure) we are studying.
Since we'll repeat this compute for two sectors: that where the robot stands and the sector where it is going to move, it's better type this code in a little function, so we'll be able to use it for both sectors and also in other situations.
We could require as input argument the facing of the object (we need to know what is its facing to give a relative "rising/declivity" result), while for sector data we could suppose that the data was already in FLOOR structure, since the calling code (that it will call our new function) it will have already called the CheckFloor() function for given sector to analyse.
While for returned value we could return own a "SECTOR_" constant value we have just created.


The DiscoverSectorType() function

// returns a SECTOR_... value about flat/rising/declivity of floor sector with data in FLOOR structure
// in according with current direction (facing/orienting) of item ItemDirection
int DiscoverSectorType(short ItemDirection)
{
          if (FLOOR.SlopeType == enumSLOPE.FLAT) {
                    // the sector is flat
                    return SECTOR_FLAT;
          }
          // the sector is NOT flat, so it will be or a rising or a declivity
          // we compare the direction of rising of the sector with ItemDirection
          if (FLOOR.SlopeOrienting == ItemDirection) {
                    // the rising has same direction of the item
                    return SECTOR_RISING;
          }
          // the rising has different direction of the item, so it will be like a declivity for the item
          return SECTOR_DECLIVITY;
}


How to monitor current animation

Before showing the other code to manage the change of robot's feet, we need to describe a problem in situation like this.
When we discover the status of current sector and next sector we'll be able to choose the animation to force to move in right way the feet, but we need handle this problematic situation:

  1. We discover that current sector is flat while next sector is a rising.
    So we set the animation 1 (From flat feet to rising) in robot.

  2. At next frame the robot will be moved a bit closer to next sector but probably the situation is yet the same of previous frame: current sector is flat and next is a rising.
    If now we set newly the animation 1, we'll force also to restart from frame 0 of this animation, and this is a bug, because we are freezing that animation always on first frame because we set continuosly the same animation from scratch frame by frame.


To solve this problem there is a simple solution: before setting the required animation we'll verify if that same animation number is already set in the robot. If it is, it will be not necessary force it newly.
Perform this test it's easy but we have to remember that value in "AnimationNow" variable of item structure is an "absolute" animation index, while we need to know the relative index, relative in according with first animation for robot object.
Since the absolute animation index is the index of the animation record in whole list of animation for whole level (and for all moveables) we need to subtract from that absolute value, the index of first animation for our robot, in this way we'll discover the relative animation index, where "0" it will be first animation of the robot, "1" the second ect.
Since this target will be required very often, we could create another little function to discover the realtive animation index of a given moveable.
This function will have as input the item structure of the moveable, and it will return the relative index of current "AnimationNow".


The GetRelativeAnimation() function

int GetRelativeAnimation(StrItemTr4 *pItem)
{
          StrSlot *pSaveSlot;
          int FirstAnimation;
          int RelativeIndex;

          // save the slot structure in GET structure to avoid to change this value that it could be used from calling code
          pSaveSlot = GET.pSlot;

          Get(enumGET.SLOT, pItem->SlotID,0);

          FirstAnimation = GET.pSlot->IndexFirstAnim;

          // restore slot structure
          GET.pSlot = pSaveSlot;

          RelativeIndex = pItem->AnimationNow - FirstAnimation;

          return RelativeIndex;
}



The code to turn the Robot's feet

Now we can add to Control() procedure the code to set right animations to turn vertically the feet of the robot when it is going to move over a different sector type.
Immediatly after the CheckFloor() of current position of the robot, we can get and store the value of first sector type:

          if (GET.pItem->SpeedH > 0) {
                    // the robot is moving: check for freeway and collisions

                    // discover infos about floor of current position of robot
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);

                    // discover the type of current sector
                    FirstSectorType = DiscoverSectorType(GET.pItem->OrientationH);

Now in FirstSectorType there will be one of SECTOR_ types we saw above.
While, to compute the next sector type, we'll type our code AFTER the contol about free way, to work on updated values, since the direction of the robot it could have been changed by above code about free way direction.

                    // if the robot is moving forward (it's NOT turning on itself, or moving the head) analyse next
                    // sector where it is going, to change animation to turn feet in according with sector types
                    if (GET.pItem->StateIdCurrent == 0 || GET.pItem->StateIdCurrent == 1) {
                              // robot is moving forward
                              // discover the point of its border in current direction of moving
                              GetIncrements(GET.pItem->OrientationH, &IncX, &IncZ, GET.pItem->SpeedH + 256);

                              x = GET.pItem->CordX + IncX;
                              y = GET.pItem->CordY;
                              z = GET.pItem->CordZ + IncZ;

                              CheckFloor(x,y,z, GET.pItem->Room);

                              NextSectorType = DiscoverSectorType(GET.pItem->OrientationH);

In above code we used a condition on "StateIdCurrent" to work only when the state id is "0" or "1".

indeed, 0 and 1, are the only state ids when the robot is moving forward and we should change the feet vertical orienting only when it is moving forward and not when it is turning on itself or turning or moving up/down its head.

Then we used GetIncrements() function to discover the 3d point where is its border in the direction where it is moving. We added 256 to current speed because 256 is (about) the half size of the robot.

Now we have sector type for current sector and next sector, and the following code will use this data to discover the right animation to set to change feet inclination:

                              NewAnimation= -1; // set -1 to mean: do not change animation of robot

                              if (FirstSectorType != NextSectorType) {
                                        // only when the two sectors have different type we'll begin the animation to change feet
                                        // now the sectors are differents: discover the number of animation required:
                                        switch (NextSectorType) {
                                        case SECTOR_FLAT:
                                                  // next it will be FLAT, now discover if current was rising or declivity
                                                  if (FirstSectorType == SECTOR_RISING) {
                                                            // it was a rising: we choose animation 5 (Feet from rising to flat.)
                                                            NewAnimation= 5;
                                                  }else {
                                                            // it had to be a declivity: choose the animation 6 (Feet from declivity to flat.)
                                                            NewAnimation= 6;
                                                  }
                                                  break;
                                        case SECTOR_RISING:
                                                  // next is rising, first had to be "flat" becasue it's not foreseen change between rising to decivlity
                                                  // we choose anmation 1 (From flat feet to rising)
                                                  NewAnimation = 1;
                                                  break;

                                        case SECTOR_DECLIVITY:
                                                  // next is a declivity, the first had to be "flat" because it's not foreseen change between decivlity to rising
                                                  // we choose the animation 3 (Feet from flat to declivity.)
                                                  NewAnimation = 3;
                                                  break;
                                        }
                                        
                              }

In above code we used a Switch() statement, to check all three conditions about when NextSectorType is == SECTOR_FLAT, or == SECTOR_RISING or == SECTOR_DECLIVITY.

Now we have in NewAnimation (local) variable, the number of animation to set, but we'll use really this value only if it is different by current animation of robot, because, in the opposite situation that it was the same, we should avoid to force newly same animation because its frame will come back to "0", freezing the animation.

                              // now we force new animation only if it is different than "-1" (no animation has been set)
                              // and different from current animation of robot (we had already set in previous frame)
                              if (NewAnimation != -1 &&
                                        NewAnimation != GetRelativeAnimation(GET.pItem)) {

                                        ForceAnimationForItem(GET.pItem, NewAnimation, -1);
                              }


About new local variables we added to ControlRobotStarWars() function, they are:

          int FirstSectorType;
          int NextSectorType;
          int NewAnimation;
          int IncX;
          int IncZ;
          DWORD x;
          int y;
          DWORD z;


Now we can build the project and discover how it works...




It works enough fine.
Probably we should get faster the vertical turning of feet, anyway this fixing it should be done in animation editor, changing the feet animation, it's not a problem about the code.


How to detect the collision with static items

We solved the problem about feet on the slopes, now we find another problem to solve.


We see in above image what happened.
The robot ignores the static (the pilar) and enter in that sector and paste itself in the static.
To avoid this situation we have to improve the IsFreeWay() function to recognize also the static as obstacles.

We'll use the Find() function with FIND_STATICS_SECTOR type.
Theoratically it should enough verify if it exists at least one static in next sector where we are looking for free way, but we'll do more advanced computation: if the static is a shatter we'll set as freeway that sector because the robot it will be able to shatter it, while when there is at least one static that was not a shatter it will be considered an obstacle.
We add at end of IsFreeWay() function the control about static items:

          // now we check for static items in MyPos position
          Find(enumFIND.STATICS_SECTOR, -1, FLOOR.RoomIndex, -1, 0, &MyPos);

          // save previous static item structure in the case it was used by calling code (to preserve it, restoring it at end)
          pSaveStatic = GET.pStatic;
          for (i=0; i<FIND.TotStatics;i++) {
                    Get(enumGET.STATIC, FIND.VetStatics[i].RoomIndex, FIND.VetStatics[i].ObjIndex);
                    TestIgnore=false;
                    // we have to verify that this static is visible (active) and it has collisions
                    if ((GET.pStatic->OCB & OCBS_ATTIVO) == 0) {
                              // this static has been already destroied or removed. Set to ignore it as it was mising:
                              TestIgnore=true;
                    }
                    if (GET.pStatic->OCB & OCBS_NO_COLLISIONI) {
                              // this static has no collision. Ignore it:
                              TestIgnore=true;
                    }                    
                    // if this static is a shatter we'll ignore it
                    if (GET.pStatic->SlotId >= Trng.pGlobTomb4->pBaseCustomize->ShatterInizio &&
                              GET.pStatic->SlotId <= Trng.pGlobTomb4->pBaseCustomize->ShatterFine) {
                              // this is a shatter: ignore it
                              TestIgnore=true;
                    }
                    // now, if this statis is NOT to ignore it is an obstacle: no freeway
                    if (TestIgnore==false) {
                              // restore static structure in GET variable
                              GET.pStatic = pSaveStatic;
                              return false;
                    }
                    // it was to ignore, we go on to check other statics in this sector
          }
          // restore static structure in GET variable
          GET.pStatic = pSaveStatic;
          return true;
}

We added also two new local variable to manage the new code:

          StrMeshInfo *pSaveStatic;
          bool TestIgnore;


Now we try this new code...



Now the robot turns when it meets the pilar.
Anyway since we set to ignore the shatter object we need to type another code to destroy these shatters when the robot move on their sector.
In this case we need to perform a more precise check for collision, because it should be weird if the shatter was destroied before robot touched it only because it (the robot) is going to move on that sector.
In our test level for Star Wars robot there is also a shatter object


And the robot ignores it, own as we set in IsFreeWay() function.
But now we have to manage this situation, destroying the shatter when the robot collides with it.
The code to get this target we cann't place in IsFreeWay() function becase the matter is different now.
We'll add this code to ControlRobotStarWars() function, anyway since this target to discover if a moveable item is colliding with some static it could be useful also in other circustances, we'll create a new little function to reach this target.
Note: in Function Collection of plugin, there is a function named "IsCollideWithStatic()" but this function works only when you already know the index of static to check, while now we need to discover what statics are colliding with our moveable item.
Our new function will have the target to discover the static items close to the item, and then, using the IsCollideWithStatic() function, to discover if there is really a collision.
We could name our new function "DetectCollisionWithStatics".
This function will require as input the index of the moveable item structure, and then it will return the static indices (statics have two indices: that of the room and that of the static item) and the "true" value to signal a collision.
When there will be no collision, it will return "false" and "-1" as static indices.
So the declare of our function it will be:

bool DetectCollisionWithStatics(int ItemIndex, int * pStaticIndex, int *pStaticRoom);

Please note that we don't consider the chance that the moveable item is colliding with two or more static items, in spite it could be possible.
This is not a great limitation, since we'll manage other (further) statics in next frames, aftet to have destroyed the first found.


The DetectCollisionWithStatics() function

bool DetectCollisionWithStatics(int ItemIndex, int * pStaticIndex, int *pStaticRoom)
{
          int i;
          int IndexStatic;
          int RoomStatic;

          // set to -1 the static indices to singal "no collision"
          *pStaticIndex=-1;
          *pStaticRoom=-1;
          
          Get(enumGET.ITEM, ItemIndex,0);

          // find all statics in the range of two sector, closed to pItem position

          Find(enumFIND.STATICS_NEARBY, -1, GET.pItem->Room, -1, 2048, &GET.pItem->CordX);

          for (i=0;i<FIND.TotStatics;i++) {
                    IndexStatic = FIND.VetStatics[i].ObjIndex;
                    RoomStatic = FIND.VetStatics[i].RoomIndex;

                    // verifiy if there is a collision
                    if (IsCollideWithStatic(ItemIndex, IndexStatic, RoomStatic, 0)==true) {
                              // found a static that is colliding with moveable item
                              // set indices of statics in pointer supplied as input to return these values to calling code
                              *pStaticIndex = IndexStatic;
                              *pStaticRoom = RoomStatic;
                              return true;
                    }
          }
          return false;
}

Note: since the arguments in the function are two pointers "int * pStaticIndex" and "int *pStaticRoom", this means that when we call this function we should use a code like this:

          int IndexOfStatic;
          int IndexOfRoom;

          DetectCollisionWithStatics(43, &IndexOfStatic, &IndexOfRoom);

And now in IndexOfStatic and IndexOfRoom local variables, there will be the indices of statics (or -1 if there is no collision).

Now we use above function inside of ControlRobotStarWars() function.
We should perform this control, everytime the robot is moving, and not only when at center of the sector.

So we'll add this new code immediatly after the instruction:

          if (GET.pItem->SpeedH > 0) {
                    // the robot is moving: check for freeway and collisions

We need of some new local variable in out ControlRobotStarWars() function:

          int StaticIndex;
          int StaticRoom;
          int NgleStaticIndex;

To do shatter the object we'll use the flipeffect 160:

; Set Trigger Type - FLIPEFFECT 160
; Exporting: TRIGGER(100:0) for FLIPEFFECT(160) {Tomb_NextGeneration}
; <#> : Statics. Explosion. Shatter <&>static
; <&> : SHATTER1 ID 100 in sector (1,5) of Room16
; (E) :

PerformFlipeffect(NULL, 160, 100, 0);

But now we have a little problem...
When you, from ngle program, perform a flipeffect to affect a static, the program will set a static index that is a ngle static idex, i.e. a single index that tom4 engine will convert to double index: room index (where is the static) and static index inside of that room.
Since in our code we'll find a double (tomb4 native) static index, before calling PerformFlipeffect() function, we have to convert this double index to a single ngle index.
We'll use the CreateNgleStaticIndex() function to create a temporary ngle static index.
This will be the new code:

          if (GET.pItem->SpeedH > 0) {
                    if (DetectCollisionWithStatics(ItemIndex, &StaticIndex, &StaticRoom) == true) {
                              // there is collision with some static.
                              // we already know that it is a shatter since for other static types, we'll have avoided it.
                              // we use flipeffect 160 to destroy the shatter:
                              // get a temporary ngle static index to pass it to PerformFlipeffect() function
                              NgleStaticIndex = CreateNgleStaticIndex(StaticRoom, StaticIndex);
                              PerformFlipeffect(NULL, 160, NgleStaticIndex, 0);
                    }


Now we build the project and play the game...




It works...


How to detect box (gray) sectors

In tom4 engine there is a way to stop the access to some zone to enemies avoiding to place some static or other obstacle on that sector: using a box sector.
It is a smart idea, because in this way we let free lara to move on that sector in spite the enenemies will be not able to do.


In our level we have a box sector, you see in above position the gray sector at right of the robot.
Anyway, currently the robot will move over that sector, after a long trip, because we have not typed a code to detect the presence of box sector.
To realize this target we should, again, change the IsFreeWay() function, and consider as an obstacle the presence of a box in next sector.
We discover the presence of box (gray) sector, using CheckFloor() function. Then in the FLOOR structure, we can check the "TestGraySector" variable to verify if that sector is a gray box.
Since we condiser "busy" the way for many situations: the presence of a moveable or a static, the height of floor or ceiling ect, the rule is to check at begin the condition more faster to be performed and only later, the others. In this way we'll get a faster code, everytime the first condition is already true and we'll have no need to check other, more long, conditions.
For this reason we'll type this control about box sector, first to check for statics or moveable, because the check for box sector is faster than discover the presence of items.
So, in IsFreeWay() function, immediatly after this code:

          CheckFloor(NewX, NewY, NewZ, GET.pItem->Room);

          if (FLOOR.TestFullWall == true) {
                    // cube stopped by a wall
                    return false;
          }

          if (TopSideY < FLOOR.CeilingHeight) {
                    // the object touch the ceiling: no free way
                    return false;
          }

We'll add new condition to detect the presence of a box sector:


          if (FLOOR.TestGraySector == true) {
                    // it is a gray box sector: no free way
                    return false;
          }


In this way the robot will avoid to pass over box sectors.


How to read OCB settings

To customize the robot, level builders will have the chance to set some ocb flag in OCB field of the object to affect its behaviour.
We remember that a flag is always a power by 2 value, i.e. a value like: 1, 2, 4, 8, 16, 32, 64, 128 ect.
As first ocb flag we could set the value to enable the "supervisory control".
As usual it's better assing a mnemonic constant to ocb values to recognize them easier in the code.
We can use constants begin with OCB_SWR_ where SWR means Star Wars Robot, of course.
So our first mnemonic constant it will be:

#define OCB_SWR_SUPERVISORY 0x0001

We type above row in "Constants_mine.h" source.

To verify if level builder had inserted above flag in ocb of the robot, we'll use this code:

          if (GET.pItem->OcbCode & OCB_SWR_SUPERVISORY) {
                    // it has been enabled the supervisory control

          }


How to detect the presence of Lara

When in OCB field of star wars robot there will be the OCB_SWR_SUPERVISORY flag, the robot will try to detect the presence of Lara and when it happens some action it will be performed.
We already know the condition trigger 84:

; Set Trigger Type - CONDITION 84
; Exporting: CONDITION(84:58) for OBJECT(-1) {Tomb_NextGeneration}
; <#> :
; <&> : Creature. <#>Moveable with (E)degrees of view is able to see Lara
; (E) : 45 degrees

We'll use something of alike, but we prefer using the native function to perform this compute.


The CheckDirection() function
The CheckDirection() function verify if in the line linking two items there are obstacles or less.
When there is no obstacle it means that first item is able to see the second item.


This function requires many input parameters:

bool CheckDirection(StrItemTr4 *pSourceItem, int OffSourceY, int TargetIndex, int OffTargetY, short TolleranceH, short TolleranceV);

pSourceItem argument
-------------------------------
This is a pointer of a moveable item structure.
For instance if you wish check the chance of Lara to see something, you can use this sytntax:

// get lara structure
Get(enumGET.LARA, 0,0);
CheckDirection( GET.pLara, ....


OffSourceY argument
----------------------------
This is an offset to change the Y coordinate of origin of pSourceItem moveable.
Since this procedure will check a line, we have to set with precision the source point (and then, the target point).
Pratically you should set the OffSourceY value to move the Y origin from feet to eyes of Lara.
Since to move up in 3d world a point, we have to reduce its value, the OffSourceY will have a negative value, about even to lara's height. For instance a reasonale value is -730.

TargetIndex argument
------------------------------
This is the index of the moveable item we are analysing as target.

OffTargetY argument
----------------------------
This argument is alike the OffSourceY but to move the target point from feet (or floor position) of target item to a new position.

TolleranceH argument
------------------------------
This value will be used to compare the difference between the facing of pSourceItem and the direction of the line between pSourceItem and the target.
Pratically is a way to set the (half) of field of view of Source item.
Please note that this value is not in decimal degrees but it used the same units for Orienting (facing, direction) in the game.
This means that 0x4000 it is 90 degrees, like 0x2000 is 45 degrees.
Remember also that is max difference between soruce facing and line of fire for target, so if you wish assign to source item a field of the view of 100 degrees you should set 50 degrees as tollerance, and type this value in facing units.
To convert decimal degress to facing units you can use this formula:

FacingUnits = 182 * DecimalDegrees

TolleranceV argument
-----------------------------
This argument is alike TolleranceH but it works about vertical direction.
If target item is very higher than source item probably is not realistic that source item was able to see it, in spite it was, has horizontal view in the field of view of source item.
Also this argument work in facing units and you should use lower values because human have a limited vertical angle of view.


The code to check if the robot is able to see Lara

Now we can add the code to detect if the robot, with current facing, is able to see lara, when it happens, as first experiment, we could flash the screen only to have a confirm of this right detection.
We'll use this flipeffect to flash the screen with red color:

; Set Trigger Type - FLIPEFFECT 355
; Exporting: TRIGGER(2560:0) for FLIPEFFECT(355) {Tomb_NextGeneration}
; <#> : Screen. Flash screen with the <&>Light color for (E)Durate
; <&> : Red Light
; (E) : Fast

PerformFlipeffect(NULL, 355, 0, 10);

We chose the "fast" durate because we'll perform this computation for each frame and so durate it could be anyway infinite, until the detection is active.
We change the code of ControlRobotStarWars() function, adding the new code immediatly after the condition about triggeractive, since this check will be always performed when the right ocb (the OCB_SWR_SUPERVISORY value) is enabled.

          if (TriggerActive(GET.pItem)==false) return;

          // check if robot works in supervisory mode
          if (GET.pItem->OcbCode & OCB_SWR_SUPERVISORY) {
                    // yes
                    // now we check if robot is able to see lara.
                    Get(enumGET.LARA,0,0);
                    if (CheckDirection(GET.pItem, -700, GET.LaraIndex, -730, 0x2000, 0x2000)==true) {
                              // robot detects lara:
                              PerformFlipeffect(NULL, 355, 0, 10);
                    }
          }


Now we build the project and try...



It works. You can see that the field of view is a bit short, we chose in this way because that single eye/cam corder should have not a wide angle.
In A picture lara has been detected, while in B picture it was not because she is moved respect to forward direction of robot and the angle is too wide.


How to create our customize script commands

In previous chapter we changed the color of game scene with a red flash, anyway we should have other actions when the robot detects lara.
Ideal situation is to let to the level builder the chance to choose what do to when the robot detects Lara.
We could creare an ocb flag that means "On lara's detection perform a triggergroup..." but then we have to set where type this triggergroup number.
We could use a customize command with a new mnemonic constant, like CUST_STAR_WARS_ROBOT where level builder will be able to set value to customize the behaviour of the robot.


The plugin.script file

The new script command for your plugin will go typed in a file ".script" extension and same name of your plugin.
Since in this tutorial we are working on "pluging_trng" plugin, this file will have the name:

Plugin_trng.script

The ".script" file goes in ng_center folder, i.e. same folder where you have the "ng_center.exe" program.

As you can see, your plugin has different files with same name (that of your plugin) but different extensions. In according with extension type you'll place the file in trle folder or in ng_center folder.
If you wish, you can see a description of all file types for plugin, and their syntax and position reading Plugin Run-time Files



We have to create a customize constant for our star wars robot and since we are customize it whereby script perhaps it's better also moving the setting about supervisory from ocb to script file, because it's more logical.
For this reason we'll create also a new mnemonic constant that it will be a flag for our customize command for the robot.

Our plugin_trng.script file

We type in our .script file following text:


<START_CONSTANTS>
CUST_STAR_WARS_ROBOT:1 ;Used with customize command>Syntax: Customize=CUST_STAR_WARS_ROBOT, RobotIndex, Flags (SWR_...), ExtraParameter, TriggerGroupIdOnSupervisory >>RobotIndex field>---------------->The index of SW Robot you are customizing with this script command.>Since in the level could be more SW Robots, you have to specify the index of for any Customize=CUST_STAR_WARS_ROBOT command.>For this reason, you should have a Customize for each robot in the level, at least you don't set ANY_SW_ROBOT constant in this field, to get that, all SW Robots with no specific customize, will have same settings of this customize command.          >>Flags (SWR_...) field>---------------------->You can type here one or more SWR_ flags linke with + operator, to set different skills of robot.>Look for SWR_ constants for infos>>ExtraParameter field>-------------------->The meaning of this field it will depend by SWR_ flag used.>Look for description in specific SRW_ flag you used.>                    >TriggerGroupIdOnSupervisory field>---------------------------------->In this field you can type the id of a TriggerGroup to perform when the robot detects Lara.>You should use the SWR_SUPERVISORY flag.

ANY_SW_ROBOT:-2 ;used Used with Customize=CUST_STAR_WARS_ROBOT command>To use in RobotIndex field to set current customize for all SW Robot of current level

SWR_SUPERVISORY:$0001 ;Used with Customize=CUST_STAR_WARS_ROBOT command>This flag enable the Supervisory mode.>When SW Robot is in Supervisory mode it tries to detect the presence of Lara and when it detects her the TriggerGroup, whom id you typed in TriggerGroupIdOnSupervisory argument of Customize=CUST_STAR_WARS_ROBOT command, it will be performed.>Note: supervisory mode disable the SWR_KILLING or SWR_HURTING modes.

SWR_HURTING:$0002 ;Used with Customize=CUST_STAR_WARS_ROBOT command>This flag enable star wars robot to injure lara with electrical lightning.>You should set in ExtraParameter field the damage for lara for each frame durate of the shake.

SWR_KILLING:$0004 ;Used with Customize=CUST_STAR_WARS_ROBOT command>This flag get SW Robot is a kill-machine. It will shot lightnings that will be able to kill lara, burning her.>Note: this flag is not compatible with SWR_SUPERVISORY or SWR_HURTING flags
<END>

The text is a bit messed because has ">" character inside of carriage returns for new lines. See syntax of .script file: Plugin run-time files
Anyway expanding the ">" characters to restore original lines (you can do this using TextEditor.exe present in "tools" sub-folder of "PLUGIN_SDK_STORE", or the TrngPatcher utility described in Plugin SDK 2: the TrngPatcher program ) we can see the description of customize for star wars:

CUST_STAR_WARS_ROBOT:1 ;Used with customize command
Syntax: Customize=CUST_STAR_WARS_ROBOT, RobotIndex, Flags (SWR_...), ExtraParameter, TriggerGroupIdOnSupervisory

RobotIndex field
----------------
The index of SW Robot you are customizing with this script command.
Since in the level could be more SW Robots, you have to specify the index of for any Customize=CUST_STAR_WARS_ROBOT command.
For this reason, you should have a Customize for each robot in the level, at least you don't set ANY_SW_ROBOT constant in this field, to get that, all SW Robots with no specific customize, will have same settings of this customize command.          

Flags (SWR_...) field
----------------------
You can type here one or more SWR_ flags linke with + operator, to set different skills of robot.
Look for SWR_ constants for infos

ExtraParameter field
--------------------
The meaning of this field it will depend by SWR_ flag used.
Look for description in specific SRW_ flag you used.
                    
TriggerGroupIdOnSupervisory field
----------------------------------
In this field you can type the id of a TriggerGroup to perform when the robot detects Lara.
You should use the SWR_SUPERVISORY flag.

Now we can save the "plugin_yourname.script" file in NG_Center folder and then launch it to load the data.


How to read our Customize command data

As first experimenti we could type a command like this in the script:

Customize=CUST_STAR_WARS_ROBOT, 88, SWR_SUPERVISORY, IGNORE, 1

We have enabled the Supervisory mode on robot with (ngle) index = 88
When robot will detect lara it will be performed the TriggerGroup with ID = 1:

TriggerGroup= 1, $2000, 1, $0

This triggergroup will perform a little earthquake with exported flipeffect 1:

; Set Trigger Type - FLIPEFFECT 1
; Exporting: TRIGGER(0:0) for FLIPEFFECT(1) {Plugin_MechWarrior}
; <#> : OldFlip. Plays a fast single eartquake, sound and rumbler.
; <&> :
; (E) :
; Values to add in script command: $2000, 1, $0

Now the problem is how to read from our plugin the data typed in our Customize=CUST_STAR_WARS_ROBOT script command.


The GET_MY_CUSTOMIZE_COMMAND parameter

To find a customize command of ours just using the Get() function with GET_MY_CUSTOMIZE_COMMAND parameter.
Reading the description of this paramater in Tomb_NextGeneration.h source we understand how to use it:

#define GET_MY_CUSTOMIZE_COMMAND 10 // returns value in GET.pCust-> INPUT: Index = CUST_ value, SecondayIndex = (optional) value of first field after CUST_ value or -1 if you omit this input value

For instance to locate the customize we typed in the script:

Customize=CUST_STAR_WARS_ROBOT, 88, SWR_SUPERVISORY, IGNORE, 1

We'll have to give as input parameters of Get() function the value of customize (CUST_STAR_WARS_ROBOT) and then (optional but in this case we need of it) the first value after cust_ value, so the index of robot (88)
So we could use this code:

          Get(enumGET.MY_CUSTOMIZE_COMMAND, CUST_STAR_WARS_ROBOT, 88);


Then we'll get in GET.pCust pointer the values of that customize command.
The "CustValue" will be the value of given CUST_ constant, in our case it will be CUST_STAR_WARS_ROBOT.
While the "NArguments" will be the number of values after the first CUST_ value.
Finally, the "pVetArg" is an array with all different values (arguments).


For instance if you wish know the value of moveable index (it should be 88 in our example) we'll read the value in this way:

          int Index;

          Index = GET.pCust->pVetArg[0];

To read the flag, the next value, we'll use:

          int Flags;

          Flags = GET.pCust->pVetArg[1];

While the value of ExtraParameter will be in "GET.pCust->pVetArg[2]" and the index of TriggerGroup will be in: "GET.pCust->pVetArg[3]"

So, apparently it's easy get the data from our customize command.
Anyway looking newly that command to get the data:

          Get(enumGET.MY_CUSTOMIZE_COMMAND, CUST_STAR_WARS_ROBOT, 88);

We have yet some issue to solve...

As first point, we should remember to declare a mnemonic constant with name "CUST_STAR_WARS_ROBOT" in our "Constants_mine.h" source, assigning to it same value used in .script file, of course.
Since the value in the script was "1":

CUST_STAR_WARS_ROBOT:1 ;Used with customize command
Syntax: Customize=CUST_STAR_WARS_ROBOT, RobotIndex, Flags (SWR_...), ExtraParameter, TriggerGroupIdOnSupervisory

We have to declare in constants_mine.h the same constant:

#define CUST_STAR_WARS_ROBOT 1

Note: we have to declare also all other constants present as SWR_ flags, of course.
So we'll declare in "Constants_mine.h" source also the constants:

#define SWR_SUPERVISORY 0x0001
#define SWR_HURTING 0x0002
#define SWR_KILLING 0x0004


The second point is that using a constant value like "88" as index to locate is a foolishness since the code of our plugin in this way will work ONLY when the robot has index = 88, while we wish that our plugin was able to manage any robot with any index, of course.
For this reason we'll have to discover the index of robot we are managing in plugin code, then we'll have to convert that (tomb) index in ngle format (like that we find in customize command) and then we'll pass this ngle index variable as secondary index of Get() function to locate the customize command.

Third and last point, is that we have to consider also the chance that the level builder had not set any customize command for that robot, and so we have to verify if the Get() call had a negative (not found) result.


How to discover the ngle index of a moveable

All Control() procedure of moveables have the index of that moveable as argument:

void ControlRobotStarWars(short ItemIndex)

That input argument (ItemIndex) is in Tomb format, of course.
So to convert the tomb index to ngle index we have to use this function:

int FromTomb4IndexToNgleIndex(int TombIndex)

So, just declaring a local variable in our function (we are working into the ControlRobotStarWars() function, now) as this:

          int NgleIndex;

And then we'll use above conversion function to get the ngle index of robot on thart we are working:

          NgleIndex = FromTomb4IndexToNgleIndex(ItemIndex);

After this code, we can call the Get() function passing the NgleIndex variable to look for that specific customize with that index:

          Get(enumGET.MY_CUSTOMIZE_COMMAND, CUST_STAR_WARS_ROBOT, NgleIndex);



How to discover if Get() function failed

When you try to get some data from Get() and that it's missing (it could happen only with Customize and Parameters search) it returns "false", while when it finds the data it will return "true".
So you can verify in this way:

          if (Get(enumGET.MY_CUSTOMIZE_COMMAND, CUST_STAR_WARS_ROBOT, NgleIndex)==true) {
                    // it has been found the customize
          }else {
                    // it has NOT been found the customize
          }

Anyway there is also another method to detect a failure.
When the returned data is in some pointer ( with "p" prefix) of GET structure, has not been found, the value in that pointer will be: NULL
NULL is the 0 for pointers.
So we could verify the good result (or less) of Get() also in this other way:

          Get(enumGET.MY_CUSTOMIZE_COMMAND, CUST_STAR_WARS_ROBOT, NgleIndex);
          if (GET.pCust == NULL) {
                    // it has not been found any customize command with given input parameters
          }

          if (GET.pCust) {
                    // it has been found the customize command
          }

Please note that when you use as condition only a variable the condition will be "true" when that variable has a value different than 0 (or NULL).

Reading settings for our Star Wars Robot

At end of above descriptions we'll discover the settings for our robot, reading the customize= script command in this way:

          int NgleIndex;
          DWORD Flags;

          Get(enumGET.ITEM, ItemIndex,0);

          if (TriggerActive(GET.pItem)==false) return;

          // disover ngle index of current robot
          NgleIndex = FromTomb4IndexToNgleIndex(ItemIndex);

          Get(enumGET.MY_CUSTOMIZE_COMMAND, CUST_STAR_WARS_ROBOT, NgleIndex);

          Flags = 0;
          if (GET.pCust) {
                    // there was a customize command: read the flags
                    Flags = GET.pCust->pVetArg[1];
          }

Above is at top of ControlStarWars() function.
We saved the flags in new local variable "Flags".
In this way, if the customize command was missing the Flags will have value = 0, while if customize existed there will the flags in Flags variable.

Now the new code to verifiy if supervisory mode is enabled and to manage it, it will be:

          // check if robot works in supervisory mode
          if (Flags & SWR_SUPERVISORY) {
                    // yes
                    // now we check if robot is able to see lara.
                    Get(enumGET.LARA,0,0);
                    if (CheckDirection(GET.pItem, -700, GET.LaraIndex, -730, 0x2000, 0x2000)==true) {
                              // robot detects lara:
                              // perform the triggergroup with id stored in customize command
                              PerformTriggerGroup(GET.pCust->pVetArg[3]);
                    }
          }


Testing this new version of the code you can verify that it works fine: everytime the robot is (about) in front of lara there is the earthquake engaged by our triggergroup 1.


How to compute facing of the head of Robot

Above code worked fine but we have not yet used all animation set of SW robot.
We have to enable animations with the robot is turning its head to look at left and right.
In this situation the current code will fail, because the function we are using to detect if robot is able to see lara (the CheckDirection() function) will use the facing of the object (our SW robot) but when the robot is looking (for instance) at north but its head is looking by 90 degrees at right, the direction should be east and not north.

To solve this problem we have to perform following operations:

  1. Discover the relative turning of the head, i.e. the degrees at its left or right
  2. Add this turning to current facing of robot's body to get the absolute direction to check
  3. Replace temporarily the facing of the robot with this new direction, to get that the CheckDirection() function worked with wished direction
  4. After the call of CheckDirection() we'll restore the previous original facing of the robot



How to discover the facing of a single mesh of moveables


In spite we can access to single mesh structure that arrange the moveable, the facing field in these structures is always the same, that is the basic position of the moveable.
The turning of meshes will be performed dynamically and I don't believe that value is present in some structure we can read.
For this reason we have to use a little computation to discover what is the facing of head in a given moment (or "frame")
Since we can study the animation in Animation Editor of wad merger, we can also discover what is the max turning for that animation, in what direction and how many frame has this animation.
Once we know all these data, we can compute the current degrees of turning with a simple ration:

TotaleFrame : TotalDegrees = CurrentFrame : CurrentDegrees

Using above ratio, we can discover the value of currentdegree in this way:

CurrentDegrees = (CurrentFrame * TotalDegrees) / TotalFrames



That's easy... but looking the turning head animation we discover to have another problem...

Turning head Animations

# Description Frames From Degrees To degrees
7 Turn head at its left by 90 degrees and comes back 60 0 -90
8 Turn head at its right by 90 degrees and comes back 61 0 +90
14 Turn head at its left by 90 degrees and comes back 61 0 -90
15 Turn head at its right by 90 degrees and comes back 61 0 +90
20 Turn head at its left by 90 degrees and comes back 60 0 -90
21 Turn head at its right by 90 degrees and comes back 60 0 +90



The problem is that these animation perform a turning forward and backward for this reason we'll have to verify if current frame is in first side of animation, where from 0 degrees it turns to +/-90 degrees or when it is in half side, where it is still on max degrees (+/-90) or when it is coming back, in third side o animation, from max degrees (+/- 90 degrees) to 0 degrees.
Ok, we are a bit unlucky with these animations, but we can solve anyway this problem, knowing that each side of animation is (about) 20 frames:
First 20 frames (from 0 to 19) move from 0 to max degrees
Middle 20 frames (from 20 to 39) is still on max degrees
Last 20 frames (from 40 to 60) moves from max degrees to 0

I'll spare to you all computes, so I'll show only final code to get this result.
Since there is some code to get this result, I'll put it in a single function named "FindHeadFacing()"
This function will return the final absolute facing (or direction) where the head of robot is looking,

Note: In the case robot has no head turning animation, the facing will be the same of robot, of course.

The FindHeadFacing() function

This is the code to discover the direction where the robot is looking.

int FindHeadFacing(StrItemTr4 *pItem)
{
          int Anim;
          int Frame;
          short HeadFacing;
          short RobotFacing;

          Anim= pItem->AnimationNow;
          Frame= GetCurrentFrame(pItem);

          HeadFacing=0;

          switch (Anim) {
          case 7:
          case 14:
                    // case to work on animations: 7 or 14 (both have same turning of head)
                    if (Frame < 21) {
                              // first phase: from 0 degrees to -90 degrees ( -0x4000 or -16384)
                              HeadFacing = (Frame * -16384) / 20;
                    }

                    if (Frame >= 21 && Frame < 41) {
                              // max facing
                              HeadFacing = -16384;
                    }

                    if (Frame >= 41) {
                              // from max facing (-16384) to 0
                              // get frame number relative to last part of animation (last 20 frames)
                              Frame = Frame - 41;
                              // invert frame number
                              Frame = 20 - Frame;
                              if (Frame < 0) Frame=0;

                              HeadFacing = (Frame * -16384) / 20;
                    }
                    break;
          case 8:
          case 15:
                    // case for animations 8 or 15 (both have same turning of head)
                    if (Frame < 21) {
                              // first phase: from 0 degrees to +90 degrees ( 0x4000 or +16384)
                              HeadFacing = (Frame * 16384) / 20;
                    }

                    if (Frame >= 21 && Frame < 41) {
                              // max facing in the middle part of animation
                              HeadFacing = +16384;
                    }

                    if (Frame >= 41) {
                              // from max facing (+16384) to 0
                              // get frame number relative to last part of animation (last 20 frames)
                              Frame = Frame - 41;
                              // invert frame number
                              Frame = 20 - Frame;
                              if (Frame < 0) Frame=0;


                              HeadFacing = (Frame * 16384) / 20;
                    }
                    break;                    
          }

          RobotFacing = pItem->OrientationH + HeadFacing;

          return RobotFacing;

}


Testing new detection code

Now we can change the old detection code:

          // check if robot works in supervisory mode
          if (Flags & SWR_SUPERVISORY) {











                    // yes
                    // now we check if robot is able to see lara.
                    Get(enumGET.LARA,0,0);
                    if (CheckDirection(GET.pItem, -700, GET.LaraIndex, -730, 0x2000, 0x2000)==true) {
                              // robot detects lara:
                              // perform the triggergroup with id stored in customize command
                              PerformTriggerGroup(GET.pCust->pVetArg[3]);
                    }
          }

To get that the horizontal orienting (direction) of the robot, read from CheckDirection() function directly from its structure, was that we computed.
Since this change will be temporary we have to save the original orienting before change it and then restore to original value after the calling of CheckDirection() function.
So our code will become:

          // check if robot works in supervisory mode
          if (Flags & SWR_SUPERVISORY) {
                    // yes
                    // now we check if robot is able to see lara.
                    Get(enumGET.LARA,0,0);
                    OldOrienting = GET.pItem->OrientationH;
                    RobotFacing = FindHeadFacing(GET.pItem);

                    GET.pItem->OrientationH = RobotFacing;

                    if (CheckDirection(GET.pItem, -700, GET.LaraIndex, -730, 0x2000, 0x2000)==true) {
                              // robot detects lara:
                              // perform the triggergroup with id stored in customize command
                              PerformTriggerGroup(GET.pCust->pVetArg[3]);
                    }
                    // restore original orienting of robot
                    GET.pItem->OrientationH = OldOrienting;
          }

We had to declare two new local variables inside of ControlRobotStarWars() function:

          short OldOrienting;
          short RobotFacing;

Note:
Another question could born looking the new code: Isn't there the risk that, changing the orienting of the robot, for a short moment, the robot will change it's direction also on game screen, creating a bad flickering effect?
Well, the answer is: no, there isn't any risk.

And this speech it's important to understand because there will be other situations where we could need to change only temporirly, facing or coordinates of some moveable, with no risk to have bad results on screen.
The reason is that until it doesn't begin the drawing phase of the game, the structure of moveables are only common variables with no immediate effect on the game scene.

The Control() procedure but also Collision() procedure work before drawing phase begun, so in the procedures of these kinds we can change and restore the values of moveable items with no real effect on the screen.

Now we should testing if our new code works fine but there is a problem: currently we have any code that force robot to change its current animation. Therefor there will be no chance that it is going to turn the head to verify how it works.
So we have to add new code, to change its animation to look around itself.

AI Features of Star Wars Robot: the Supervisory Skills

Since there are animation to get that robot looking around, at left and right, we should set when engaging these animations.
The more logical solution is to force it to look at left when there is some freespace to its left, and same speech for the right, of course.
Supposing rooms like a labyrinth of corridors, the robot should looking around when it is over a crossing:




For instance in above image we see a situation where the robot should looking at left and at right (red arrows) while it is moving on its direction (yellow arrow).
To detect this situation in game, we have to check the height of floor at left and right of the robot and when it is so low to allow to the robot to look in that direction the robot will stop and it will be engaged the right animation to look in that direction.
We'll perform this control only when robot is in the middle of current sector since only in that point it could be near to new crossing and also to save cpu time.
We have already seen as checking floor to detect collisions.
Now we can create a new function that will receive as input the item structure of the robot and the relative direction where we wish check the height of the floor respect to the robot.
In this way we'll be able to use same function to check at left (giving as input relative direction -16348 (-90 degrees)) at at right (giving ad relative direction +16384)
This function will return "true" when there is a space in sector row closet in the wished direction.
The we'll perform animation to look in that direction only this function will have found a free corridor where to look.
We could name this function IsMissingWall()
Since the robot has the chance to look around with lower head but also with higher head, we'll return a generic positive result when in wished direction it's missing a full wall, and then we'll return also the height of the floor in that point. In this way the code will be able to verify if it will be possible enable animation to move up the head and look over that floor if it is higher of current heigth of robot's head.


When perform the check about looking around?
We'll perform the check for corridors at left or right not always, for each frame, but only when there are following conditions:
  1. The state-id of robot is 0
    Because this is only state-id where it's moving on flat screen.
    Since there are missing animations with mixing of turned feet and turning head, when the ronbot is moving on slopes it will be not able to look around.
  2. The robot is at center of current sector.
    In this way we'll be also at middle of further left/right corridors
  3. The robot is not going to turn to change direction.
    indeed, it should be not logical, if the robot was forced to move turning at left and we, before this change of direction, stop the robot to look in the direction where it is going to go. Since it will look in that direction anyway in a short moment.


Different code in according with current State-id

When we have a moveable item with some complicated behaviors, it's useful perform conditions about current state-id and animation number to understand quickly what it was happening in previous frame, what is doing our object, to complete that action and to choose the new action to do now.
Keeping in mind the Meaning of State-IDs , we can know what is doing our robot and set code in right position in according with these conditions.


The IsMissingWall() function


// receive as input the object and the relative direction where to check (LookDirection), 16384 = at right 90 degrees,
// -16384 at left -90 degrees
// return false if there is a wall in that direction, or true if there is a floor over that the robot could see
// Since the robot can reach three heights, this function will return in *pHeightLimit a number to signal
// the lower height required to look in wished direction:
// 0 = lower head 725
// 1 = middle head 951
// 2 = highest head 1101
// If the height of floor is higher than max height whom the robot is able to reach, it will return false

bool IsMissingWall(StrItemTr4 *pItem, short LookDirection, int *pHeightLimit)
{
          int IncX, IncZ;
          DWORD TargetX, TargetZ;
          int DiffHeight;

          GetIncrements(LookDirection, &IncX, &IncZ, 1024);

          // find coordinate of sector in whised direction
          TargetX = pItem->CordX + IncX;
          TargetZ = pItem->CordZ + IncZ;

          // discover floor data about target point

          CheckFloor(TargetX, pItem->CordY, TargetZ, pItem->Room);


          if (FLOOR.TestFullWall == true) {
                    // there is a wall
                    return false;
          }

          // there is a floor
          // verify the difference between orgy coordinate of robot and the heigth of this floor
          DiffHeight = pItem->CordY - FLOOR.FloorHeight;

          if (DiffHeight < 730) {
                    *pHeightLimit = 0;
                    return true;
          }

          if (DiffHeight < 960) {

                    *pHeightLimit = 1;
                    return true;
          }

          if (DiffHeight < 1110) {
                    *pHeightLimit = 2;
                    return true;
          }

          // the floor is so height to be unreachable from robot: return false like it was a full wall
          return false;
}


The different heigths of Robot's head



The IsMissingWall() function, will return a value also to inform about the height of head required to look over that floor.
In above image we can see the three different heights of the robot.
For each of these heights, the robot has animations to look at left or at right.
If the function will return "false" it means that there is a wall or a floor so heigh that no animation of robot will allow to look over that floor.
When the function will return "true", it means that is possible look in that direction and in this case, the function will return also a value to say what kind of height should have the head of the robot to look over that obstacle.

How to discover the size of object in game units

Before going on with our robot, it's interesting explain a little backstage of this object.
To have the heights of the robots you saw in previous chapter, I had to discover the height in game units of the robot.
A way to get these infos, it's to change temporarily the code to add a message log to show the size of collision box in game units.
I used following function to create this log:

// show in log the height of item with given Animation and given frame
void PrintHeightOfItem(StrItemTr4 *pItem, int Animation, int Frame)
{
          StrBoxCollisione *pColl;

          ForceAnimationForItem(pItem, Animation, -1);
          SetCurrentFrame(pItem, Frame);

          pColl = GetBestFrame(pItem);

          SendToLog("LowerY=%d HigherY=%d Heigth=%d",
                                        pColl->MinY, pColl->MaxY, pColl->MaxY - pColl->MinY);
}

Using the GetBestFrame() function we can get the collision box of current frame of current animation for the given object.
Then I printed out the data about Y coordinate to discover the height of the robot, using the SendToLog() function.
To catch the log you have to launch the tomb4_log.exe utility before launch tomb4.exe engine.
At end I changed the some first rows of ControlRobotStarWars() to call above PrintHeightOfItem() function.
I called it three times, one for each different animation and height kind:

void ControlRobotStarWars(short ItemIndex)
{

          bool TestOk;
          int TopSideY;
          int FirstSectorType;
          int NextSectorType;
          int NewAnimation;
          int IncX;
          int IncZ;
          DWORD x;
          int y;
          DWORD z;
          int StaticIndex;
          int StaticRoom;
          int NgleStaticIndex;
          int NgleIndex;
          DWORD Flags;
          short OldOrienting;
          short RobotFacing;

          Get(enumGET.ITEM, ItemIndex,0);

          if (TriggerActive(GET.pItem)==false) return;

          // discover height of robot with three different height of the head (only for debug)
          PrintHeightOfItem(GET.pItem, 0,0); // low head (animation 0)
          PrintHeightOfItem(GET.pItem, 10,0); // middle height (animation 10)
          PrintHeightOfItem(GET.pItem, 13,0); // highest head (animation 13)

Once I discovered the heigth of the object, then I removed above three rows, of course.

How to improve code planning whereby distinct functions

How we saw also for robot cleaner, when the rows of code become too many, it's better divide the code in logical blocks, and place each of these blocks in a distinct function with a meaningful name.
For instance with robot cleaner at end we got this main Control() procedure:

void ControlRobotCleaner(short ItemIndex)
{
          Get(enumGET.ITEM, ItemIndex,0);

          if (TriggerActive(GET.pItem)==false) return;
          // animate the robot (turning the cables)
          AnimateItem(GET.pItem);
          Get(enumGET.LARA,0,0);
          if (GET.pLara->Health < 1) {
                    // lara is dead, stop movements of the robot
                    return;
          }
          if (GET.pItem->Reserved_38 == 1) {
                    RobotCleanerHTurning();
          }
          // if we are yet in turning phase we have to skip all code to detect new directions:
          if (GET.pItem->Reserved_38 == 0) {
                    if (RobotCheckDirections(ItemIndex)==false) return;
          }
          // check newly if it has been just changed the state id "moving/turning":
          if (GET.pItem->Reserved_38 == 0) {
                    RobotMoveAndVturning(ItemIndex);
          }
          RobotAddEffects(ItemIndex);
          RobotCheckEnemyCollisions(ItemIndex);
}

Do you see how it is compact and clear?
We should do same job also for Star Wars Robot because the code is growing and mixing all different features in same Control() function is a bit chaotic.

Restyle for Star Wars Robot Code

We extracted the block code with different targets, from ControlRobotStarWars() function, and placed in new distinct functions.
At end we got this compact ControlRobotStarWars() function


ControlRobotStarWars() function

void ControlRobotStarWars(short ItemIndex)
{
          DWORD Flags;

          Get(enumGET.ITEM, ItemIndex,0);
          if (TriggerActive(GET.pItem)==false) return;

          // read Customize setting for current robot
          Flags = RobotSW_ReadSettings(ItemIndex);

          // If robot is in supervisory, killing or hurting mode, discover if it is able to see lara in this moment:
          if (Flags & (SWR_SUPERVISORY | SWR_HURTING | SWR_KILLING)) {
                    // yes, some mode that requires dedection skills of the robot has been enabled.
                    // now we check if robot is able to see lara.
                    RobotSW_DetectAndAttack(Flags);
          }
          // if robot is moving: detect obstacles and if it is necessary change direction
          if (GET.pItem->SpeedH > 0) {
                    RobotSW_MoveAndSteer(ItemIndex);
          }
          if (GET.pItem->SpeedH > 0 && (GET.pItem->StateIdCurrent == 0 || GET.pItem->StateIdCurrent == 1)) {
                    RobotSW_TurnFeet();                    
          }
          // animate the robot
          AnimateItem(GET.pItem);
}

All littler functions "RobotSW..." we created will be placed before of ControlRobotStarWars() function, in the source.
Here we see the new functions just created...


RobotSW_ReadSettings() function

DWORD RobotSW_ReadSettings(int ItemIndex)
{
          int NgleIndex;
          DWORD Flags;

          // discover ngle index of current robot
          NgleIndex = FromTomb4IndexToNgleIndex(ItemIndex);
          Get(enumGET.MY_CUSTOMIZE_COMMAND, CUST_STAR_WARS_ROBOT, NgleIndex);
          Flags = 0;
          if (GET.pCust) {
                    // there was a customize command: read the flags
                    Flags = GET.pCust->pVetArg[1];
          }else {
                    // missing specific customize for this robot. Try to look for a generic CUST_STAR_WARS_ROBOT customize
                    // with ANY_SW_ROBOT like index
                    Get(enumGET.MY_CUSTOMIZE_COMMAND, CUST_STAR_WARS_ROBOT, ANY_SW_ROBOT);
                    if (GET.pCust) {
                              // found a generic customize for all SW robots
                              Flags = GET.pCust->pVetArg[1];
                              
                    }
          }
          return Flags;
}



RobotSW_DetectAndAttack() function

void RobotSW_DetectAndAttack(DWORD Flags)
{
          short OldOrienting;
          short RobotFacing;

          Get(enumGET.LARA,0,0);
          OldOrienting = GET.pItem->OrientationH;
          RobotFacing = FindHeadFacing(GET.pItem);
          GET.pItem->OrientationH = RobotFacing;

          if (CheckDirection(GET.pItem, -700, GET.LaraIndex, -730, 0x2000, 0x2000)==true) {
                    // robot detects lara:
                    // if survillance mode perform the triggergroup with id stored in customize command
                    if (Flags & SWR_SUPERVISORY) {
                              PerformTriggerGroup(GET.pCust->pVetArg[3]);
                    }
          }
          // restore original orienting of robot
          GET.pItem->OrientationH = OldOrienting;
}



RobotSW_MoveAndSteer() function

void RobotSW_MoveAndSteer(int ItemIndex)
{
          int StaticIndex;
          int StaticRoom;
          int NgleStaticIndex;
          bool TestOk;
          int FirstSectorType;
          int TopSideY;

          if (DetectCollisionWithStatics(ItemIndex, &StaticIndex, &StaticRoom) == true) {
                    // there is collision with some static.
                    // we already know that it is a shatter since for other static types, we'll have avoided it.
                    // we use flipeffect 160 to destroy the shatter:
                    // get a temporary ngle static index to pass it to PerformFlipeffect() function
                    NgleStaticIndex = CreateNgleStaticIndex(StaticRoom, StaticIndex);
                    PerformFlipeffect(NULL, 160, NgleStaticIndex, 0);
          }          
          // the robot is moving: check for freeway and collisions
          // discover infos about floor of current position of robot
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);

          // discover the type of current sector
          FirstSectorType = DiscoverSectorType(GET.pItem->OrientationH);

          // update Y coordinate and room of robot
          GET.pItem->CordY = FLOOR.FloorHeight;

          // we have to update room of object in the case it was changed
          UpdateItemRoom(ItemIndex);

          if (GetDistanceXZ(GET.pItem->CordX, GET.pItem->CordZ,
                                                            FLOOR.SectorCoords.MiddleX, FLOOR.SectorCoords.MiddleZ) < 16) {
                    // the robot is at middle of current sector.
                    // verify collision in next sector only now (when the robot is at middle of curent sector)                    
                    TopSideY= GET.pItem->CordY - 1000;
                    // try to move forward:
                    TestOk = IsFreeWay(ItemIndex, 0, GET.pItem->SpeedH, 511, TopSideY);
                    if (TestOk==false) {
                              // try to turn at left (at its west)
                              TestOk = IsFreeWay(ItemIndex, -16384 , GET.pItem->SpeedH, 511, TopSideY);
                              if (TestOk==true) {
                                        // ok: free way at west: set animation to turn at left of 90 degrees
                                        ForceAnimationForItem(GET.pItem, 16, -1);                                                  
                              }
                    }                    
                    if (TestOk==false) {
                              // try to turn at right (at its east)
                              TestOk = IsFreeWay(ItemIndex, 16384 , GET.pItem->SpeedH, 511, TopSideY);
                              if (TestOk == true) {
                                        ForceAnimationForItem(GET.pItem, 17, -1);
                              }
                    }
                    if (TestOk==false) {
                              // try to come back, it should be always possible since we are coming from that direction
                              TestOk=true;
                              ForceAnimationForItem(GET.pItem, 18, -1);
                    }
          }
}



RobotSW_TurnFeet() function

void RobotSW_TurnFeet(void)
{
          DWORD x;
          int y;
          DWORD z;
          int FirstSectorType;
          int NextSectorType;
          int NewAnimation;
          int IncX, IncZ;

          // robot is moving forward
          // discover infos about floor of current position of robot
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);

          // discover the type of current sector
          FirstSectorType = DiscoverSectorType(GET.pItem->OrientationH);

          // discover the point of its border in current direction of moving
          GetIncrements(GET.pItem->OrientationH, &IncX, &IncZ, GET.pItem->SpeedH + 256);

          x = GET.pItem->CordX + IncX;
          y = GET.pItem->CordY;
          z = GET.pItem->CordZ + IncZ;


          CheckFloor(x,y,z, GET.pItem->Room);

          NextSectorType = DiscoverSectorType(GET.pItem->OrientationH);

          // discover the animation required to turn feet in according with sector types
          NewAnimation= -1; // set -1 to mean: do not change animation of robot

          if (FirstSectorType != NextSectorType) {
                    // only when the two sectors have different type we'll begin the animation to change feet
                    // now the sectors are differents: discover the number of animation required:
                    switch (NextSectorType) {
                    case SECTOR_FLAT:
                              // next it will be FLAT, now discover if current was rising or declivity
                              if (FirstSectorType == SECTOR_RISING) {
                                        // it was a rising: we choose animation 5 (Feet from rising to flat.)
                                        NewAnimation= 5;
                              }else {
                                        // it had to be a declivity: choose the animation 6 (Feet from declivity to flat.)
                                        NewAnimation= 6;
                              }
                              break;
                    case SECTOR_RISING:
                              // next is rising, first had to be "flat" becasue it's not foreseen change between rising to decivlity
                              // we choose anmation 1 (From flat feet to rising)
                              NewAnimation = 1;
                              break;
                    case SECTOR_DECLIVITY:
                              // next is a declivity, the first had to be "flat" because it's not foreseen change between decivlity to rising
                              // we choose the animation 3 (Feet from flat to declivity.)
                              NewAnimation = 3;
                              break;
                    }                    
          }
          // now we force new animation only if it is different than "-1" (no animation has been set)
          // and different from current animation of robot (we had already set in previous frame)
          if (NewAnimation != -1 &&
                    NewAnimation != GetRelativeAnimation(GET.pItem)) {
                    ForceAnimationForItem(GET.pItem, NewAnimation, -1);
          }
}


Management of "looking around" phase

Also the code to manage the turning of the head and moving up or down, will be typed in a distinct function.
I named this function: RobotSW_LookAround()
Before seeing the code, it's necessary explain some issue.
The matter to handle three different heights of the head (lower or standard, middle and highest) and to be able to look at left, or at right in according with free space in these directions, it's enough complicated matter...
So, to manage better the management of these different situation, it has been necessary using one of reserved field of robot structure (the Reserved_34 field) to store a value working like a state-id, to remember what is doing the robot about "looking around" phase.

To remember better the meaning of these values I created a new serie of mnemonic constant with prefix "LOOKSW_..."
These are the possible values:

#define LOOKSW_DISABLED 0
#define LOOKSW_UP_AND_RIGHT 1
#define LOOKSW_UP_AND_LEFT 2
#define LOOKSW_AT_RIGTH 3
#define LOOKSW_AT_LEFT 4

In above names the "UP" is not for "look up" but for "moving up the head", while the "LEFT" and "RIGHT" are own for the direction where looking, of course.

Since we mean use the "Reserved_34" field, we have also to intialize it, and we should do this work in InitialiseRobotStarWars() function with this code:

          // initialise "looking around" mode to 0 (disabled) (constants with LOOKSW_ prefix)
          GET.pItem->Reserved_34 = LOOKSW_DISABLED;


Now we can see the code of this complicated function...


The RobotSW_LookAround() function


void RobotSW_LookAround(void)
{
          int HeightType;
          int AnimNow;

          // check (reading Reserved_34 field of robot structure) what phase of looking around is performing the robot

          switch (GET.pItem->Reserved_34) {
          case LOOKSW_DISABLED:
                    // no looking around in progress.
                    // now we should verify if it is possible beginning own now, the looking around...

                    // it's possible only when the robot is moving forward with lowerd head and flat feet (stateid = 0)
                    if (GET.pItem->StateIdCurrent != 0) {
                              // wrong state-id: it's not possible beginning now the looking around phase
                              return;
                    }
                    // the looking around could begin only when the robot it's near at middle of sector
                    // discover infos about floor of current position of robot
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);                    

                    if (GetDistanceXZ(GET.pItem->CordX, GET.pItem->CordZ,
                              FLOOR.SectorCoords.MiddleX, FLOOR.SectorCoords.MiddleZ) > 16) {
                              // the robot is not centered with sector: no possible now beginning looking around
                              return;
                    }
                    // it should be possible, but now it's necessary verifing if there are coorridors (free spaces)
                    // at left or rigth where to look
                    if (IsMissingWall(GET.pItem, -16384, &HeightType)==true) {
                              // it's possible look at left.
                              // now discover what kind of head height we should use:
                              switch (HeightType) {
                              case 0:
                                        // just lower head. We can begin immediatly to look at left
                                        ForceAnimationForItem(GET.pItem, 7, -1);

                                        // set new "looking around" phase:
                                        GET.pItem->Reserved_34 = LOOKSW_AT_LEFT;
                                        break;
                              case 1:
                                        // required middle height of head
                                        // since robot now has lower head, we have to change height from lower to middle with
                                        // animation 9
                                        ForceAnimationForItem(GET.pItem, 9, -1);
                                        
                                        // set "looking around" phase to remember that when head will be at middle height the
                                        // robot should turn to look at left.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_LEFT;
                                        break;
                              case 2:
                                        // required highest head.
                                        // force animation 12
                                        ForceAnimationForItem(GET.pItem, 12, -1);

                                        // set "looking around" phase to remember that when head will be at highest head the
                                        // robot should turn to look at left.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_LEFT;
                                        break;
                              }
                              return;                    
                    }

                    // it was not possible looking at left
                    // now we perform same computation for right direction

                    if (IsMissingWall(GET.pItem, 16384, &HeightType)==true) {
                              // it's possible look at right.
                              // now discover what kind of head height we should use:
                              switch (HeightType) {
                              case 0:
                                        // just lower head. We can begin immediatly to look at right
                                        ForceAnimationForItem(GET.pItem, 8, -1);

                                        // set new "looking around" phase:
                                        GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;
                                        break;
                              case 1:
                                        // required middle height of head
                                        // since robot now has lower head, we have to change height from lower to middle with
                                        // animation 9
                                        ForceAnimationForItem(GET.pItem, 9, -1);
                                        
                                        // set "looking around" phase to remember that when head will be at middle height the
                                        // robot should turn to look at right.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;
                                        break;
                              case 2:
                                        // required highest head.
                                        // force animation 12
                                        ForceAnimationForItem(GET.pItem, 12, -1);

                                        // set "looking around" phase to remember that when head will be at highest head the
                                        // robot should turn to look at right.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;
                                        break;
                              }
                              return;
                    }
                    break;
          case LOOKSW_UP_AND_LEFT:
                    // we had set to move up the head and then to look at left.
                    // now we should verify if the move up movenent has been completed
                    // since when the move up with animations 9 or 10, will be completed when the next animation
                    // has been started, just checking if current animation if the current animation is:
                    // animation 10 (fixed with head at middle height)
                    // or
                    // animation 13 (fixed with highest head)
                    AnimNow = GetRelativeAnimation(GET.pItem);

                    if (AnimNow == 10) {
                              // completed: the head is at middle height. now force animation to look at left
                              // with middle height head (animation 20)
                              ForceAnimationForItem(GET.pItem, 20, -1);
                              // set new phase

                              GET.pItem->Reserved_34 = LOOKSW_AT_LEFT;
                              return;
                    }
                    if (AnimNow == 13) {
                              // completed. the head is fixed at highest position.
                              // now force animation to do look at left with highest head (animation 14)
                              ForceAnimationForItem(GET.pItem, 14, -1);
                              // se new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_LEFT;
                              return;

                    }
                    break;
          case LOOKSW_UP_AND_RIGHT:
                    // we had set to move up the head and then to look at right
                    // now we should verify if the move up movenent has been completed
                    // since when the move up with animations 9 or 10, will be completed when the next animation
                    // has been started, just checking if current animation is:
                    // animation 10 (fixed with head at middle height)
                    // or
                    // animation 13 (fixed with highest head)
                    AnimNow = GetRelativeAnimation(GET.pItem);

                    if (AnimNow == 10) {
                              // completed: the head is at middle height. now force animation to look at right
                              // with middle height head (animation 21)
                              ForceAnimationForItem(GET.pItem, 21, -1);
                              // set new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;
                              return;
                    }

                    if (AnimNow == 13) {
                              // completed. the head is fixed at highest position.
                              // now force animation to do look at right with highest head (animation 15)
                              ForceAnimationForItem(GET.pItem, 15, -1);
                              // se new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;
                              return;
                    }
                    break;
          case LOOKSW_AT_LEFT:
          case LOOKSW_AT_RIGTH:
                    // robot is turning head at left or right
                    // now verify if it has been completed.
                    // since at end of turning animation there will be one of following animations:
                    // animation 0 (after turning with lower head)
                    // animation 10 (after turning with middle head)
                    // animation 13 (after turning with highest head)
                    // just check if it has been reached one of above animations
                    AnimNow = GetRelativeAnimation(GET.pItem);

                    switch (AnimNow) {
                    case 0:
                              // completed with lower head
                              // we cam immediatly close the "looking around phase" and come back to common movements
                              GET.pItem->Reserved_34 = LOOKSW_DISABLED;
                              break;
                    case 10:
                              // completed with midle head.
                              // before complete we have to move down the head
                              // we use animation 11 (from middle height to lower head)
                              ForceAnimationForItem(GET.pItem, 11, -1);
                              // since the next animation, after animation 11 is own the animation 0 to move forward
                              // we can already disable "looking around"
                              GET.pItem->Reserved_34 = LOOKSW_DISABLED;
                              break;
                    case 13:
                              // completed with higher head

                              // before complete we have to move down the head to lower position with animation 22
                              ForceAnimationForItem(GET.pItem, 22, -1);
                              // since the next animation, after animation 11 is own the animation 0 to move forward
                              // we can already disable "looking around"
                              GET.pItem->Reserved_34 = LOOKSW_DISABLED;
                              break;
                    }
          }
}


Debugging the Looking Around feature

Now copy all new version of fuctions in your source, build the project, copy the .dll in trle folder and try in game how the "looking around" feature works...

Trying in game this new code we discover an interesting fact: pratically nothing is working fine...

Well, it happens, at first draft code.

I could fix all bugs myself, and then show to you only the final working code but this bad code is an opportunity to learn how to debug the code of your plugin.

When you built advanced scripting with triggergroup and organizer probably you had already this problem: the script didn't work like you believed and you had to discover where is the problem.
Usually you used many test and some log file, to verify what value have some variable (if you used trng variables) or watching the log of script with different script commands showed in a log to read and study to understand the problem.

Well, with plugins you can do something of better...

Visual Express is not only a tool to build your project but it works also as debugger.

How to use the Debugger of Visual Express 10

With debugger you can perform a single code row at once and to see what is the value of each variable or field of any structure.
You can set conditional breakpoint to begin the debugging only when it happens in game the phase to study.
I prepared a Debugger Tutorial where I'll use as example own the current code and bugs to fix.
I suggest you to read this turorial before going on on current page, since the tutorial is a natural prosecution of current topic: fixing the bug about "looking around" feature for star wars robot, using the debugger.
More, the debugger is very useful own to understand better how C++ instructions and trng/tomb4 function work.


New SW Robot code after debugger fixing

Afer debugging we fixed all bugs about looking around and detecting skill of robot.

Here you find new code after fixing...


Created new function: the SetAnimationAndSpeed() function
Copy the code of this function above of RobotSW_LookAround() function

void SetAnimationAndSpeed(StrItemTr4 *pItem, int NAnimation, int NextStateId, short HSpeed)
{
          ForceAnimationForItem(pItem, NAnimation, NextStateId);
          pItem->SpeedH = HSpeed;
}


Create new mnemonic constat: LOOKSW_ENDED_LEFT_TRY_RIGHT
It has been added a new constant to "Constants_mine.h" source:

#define LOOKSW_ENDED_LEFT_TRY_RIGHT 5

To type after other LOOKS_ constants in "Constants_mine.h" source

Code of RobotSW_DetectAndAttack() function after bug fixing

void RobotSW_DetectAndAttack(DWORD Flags)
{
          short OldOrienting;
          short RobotFacing;
          int HeadHeight;
          int Anim;
          int YPosCamOfRobot;
          int YPosTarget;
          int TopY;
          int BottomY;
          int MinTopY;
          StrBoxCollisione *pColl;

          Get(enumGET.LARA,0,0);
          OldOrienting = GET.pItem->OrientationH;
          RobotFacing = FindHeadFacing(GET.pItem);

          GET.pItem->OrientationH = RobotFacing;

          Anim=GetRelativeAnimation(GET.pItem);
          switch (Anim) {
          case 13:
          case 14:
          case 15:
                    // with animations 13, 14 or 15 the robot has highest head
                    HeadHeight = -1101;
                    break;
          case 10:
          case 20:
          case 21:
                    // with animation 10, 20 or 21 the robot has middle height
                    HeadHeight = -951;
                    break;

          default:
                    // with all other animation we'll use height for lower head
                    HeadHeight= -725;
                    break;
          }

          YPosTarget = -730;

          // verify if distance between lara and the robot is less than two sectors
          if (GetMaxDistance(&GET.pItem->CordX, &GET.pLara->CordX, true) <= 2048) {

                    // compute the Y position of the robot's cam:
                    YPosCamOfRobot = GET.pItem->CordY + HeadHeight;
                    // we discover the height of lara with current animation
                    pColl = GetBestFrame(GET.pLara);
                    MinTopY = pColl->MinY;
                    // compare the Y position of robot's cam with range of lara in vertical from feet Y (bottomY) to eyes (topY)
                    TopY = GET.pLara->CordY + MinTopY; // MinTopY is already a negative value, so we'll use "+" sign
                    BottomY = GET.pLara->CordY;

                    if (YPosCamOfRobot <= BottomY && YPosCamOfRobot >= TopY) {
                              // YPosCamOfRobot is inside the range about heigth of lara
                              // compute the YPosTarget (vertical offset for target) to get
                              // the same Y coordinate of source point
                              YPosTarget = YPosCamOfRobot - BottomY;
                    }else {
                              // YPosCamOfRobot is outside of range
                              // choose the value between topy and bottomy more near to YPosCamOfRobot
                              if (AbsDiffY(YPosCamOfRobot, TopY) < AbsDiffY(YPosCamOfRobot, BottomY)) {
                                        // use topY (eyes)
                                        YPosTarget = MinTopY;
                              }else {
                                        // use bottomY (feet) but a bit over
                                        YPosTarget = -256;
                              }
                    }
          }

          if (CheckDirection(GET.pItem, HeadHeight, GET.LaraIndex, YPosTarget, 0x2000, 0x2000)==true) {
                    // robot detects lara:
                    // if survillance mode perform the triggergroup with id stored in customize command
                    if (Flags & SWR_SUPERVISORY) {
                              PerformTriggerGroup(GET.pCust->pVetArg[3]);
                    }
          }
          // restore original orienting of robot
          GET.pItem->OrientationH = OldOrienting;
}



New code of RobotSW_LookAround() function after bug fixing

void RobotSW_LookAround(void)
{
          int HeightType;
          int AnimNow;
          int OldStatus;

          // check (reading Reserved_34 field of robot structure) what phase of looking around is performing the robot

          switch (GET.pItem->Reserved_34) {
          case LOOKSW_ENDED_LEFT_TRY_RIGHT:
                    // when there is this status we'll have to check if it is possible looking at right
                    // but we have to wait that the previous looking at left has been completed.
                    // so, since after the looking at left there will be animation 19 with stateid = 6
                    // we now verify if the robot has right stateid to begin a new "looking" operation
                    if (GET.pItem->StateIdCurrent != 6) return;

                    // now we'll do the control at right but we have to remove the LOOKSW_ENDED_LEFT_TRY_RIGHT status because once we checked
                    // if there is no chance to look at right we have alredy completed that control

                    GET.pItem->Reserved_34 = LOOKSW_DISABLED;

                    // now we copy same code of "case LOOKSW_DISABLED:" but only the part to check at right:
                    // now we perform same computation for right direction

                    if (IsMissingWall(GET.pItem, 16384, &HeightType)==true) {
                              // it's possible look at right.
                              // now discover what kind of head height we should use:
                              switch (HeightType) {
                              case 0:
                                        // just lower head. We can begin immediatly to look at right
                                        SetAnimationAndSpeed(GET.pItem, 8, -1,0);

                                        // set new "looking around" phase:
                                        GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;
                                        break;
                              case 1:
                                        // required middle height of head
                                        // since robot now has lower head, we have to change height from lower to middle with
                                        // animation 9
                                        SetAnimationAndSpeed(GET.pItem, 9, -1,0);
                                        
                                        // set "looking around" phase to remember that when head will be at middle height the
                                        // robot should turn to look at right.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;
                                        break;
                              case 2:
                                        // required highest head.
                                        // force animation 12
                                        SetAnimationAndSpeed(GET.pItem, 12, -1,0);

                                        // set "looking around" phase to remember that when head will be at highest head the
                                        // robot should turn to look at right.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;
                                        break;
                              }
                              
                    }

                    if (GET.pItem->Reserved_34 == LOOKSW_DISABLED) {
                              // it's not possible looking at right.
                              // so, now we set newly the animation 0 to move forward the robot
                              SetAnimationAndSpeed(GET.pItem, 0,-1,32);

                    }
                    break;
          case LOOKSW_DISABLED:
                    // no looking around in progress.
                    // now we should verify if it is possible beginning own now, the looking around...


                    // it's possible only when the robot is moving forward with lowerd head and flat feet (stateid = 0)
                    if (GET.pItem->StateIdCurrent != 0 && GET.pItem->StateIdCurrent != 1) {
                              // wrong state-id: it's not possible beginning now the looking around phase
                              return;
                    }
                    // the looking around could begin only when the robot it's near at middle of sector
                    // discover infos about floor of current position of robot
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);                    

                    if (FLOOR.SlopeType != enumSLOPE.FLAT) {
                              // only from flat sectors we can look around
                              return;
                    }

                    if (GetDistanceXZ(GET.pItem->CordX, GET.pItem->CordZ,
                              FLOOR.SectorCoords.MiddleX, FLOOR.SectorCoords.MiddleZ) > 16) {
                              // the robot is not centered with sector: no possible now beginning looking around
                              return;
                    }

                    // it should be possible, but now it's necessary verifing if there are coorridors (free spaces)
                    // at left or rigth where to look
                    if (IsMissingWall(GET.pItem, -16384, &HeightType)==true) {
                              // it's possible look at left.
                              // now discover what kind of head height we should use:
                              switch (HeightType) {
                              case 0:
                                        // just lower head. We can begin immediatly to look at left
                                        SetAnimationAndSpeed(GET.pItem, 7, -1,0);

                                        // set new "looking around" phase:
                                        GET.pItem->Reserved_34 = LOOKSW_AT_LEFT;
                                        break;
                              case 1:
                                        // required middle height of head
                                        // since robot now has lower head, we have to change height from lower to middle with
                                        // animation 9
                                        SetAnimationAndSpeed(GET.pItem, 9, -1,0);
                                        
                                        // set "looking around" phase to remember that when head will be at middle height the
                                        // robot should turn to look at left.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_LEFT;
                                        break;
                              case 2:
                                        // required highest head.
                                        // force animation 12
                                        SetAnimationAndSpeed(GET.pItem, 12, -1,0);

                                        // set "looking around" phase to remember that when head will be at highest head the
                                        // robot should turn to look at left.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_LEFT;
                                        break;
                              }
                              return;                                        
                    }

                    // it was not possible looking at left
                    // now we perform same computation for right direction

                    if (IsMissingWall(GET.pItem, 16384, &HeightType)==true) {
                              // it's possible look at right.
                              // now discover what kind of head height we should use:
                              switch (HeightType) {
                              case 0:
                                        // just lower head. We can begin immediatly to look at right
                                        SetAnimationAndSpeed(GET.pItem, 8, -1,0);

                                        // set new "looking around" phase:
                                        GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;
                                        break;
                              case 1:
                                        // required middle height of head
                                        // since robot now has lower head, we have to change height from lower to middle with
                                        // animation 9
                                        SetAnimationAndSpeed(GET.pItem, 9, -1,0);
                                        
                                        // set "looking around" phase to remember that when head will be at middle height the
                                        // robot should turn to look at right.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;
                                        break;
                              case 2:
                                        // required highest head.
                                        // force animation 12
                                        SetAnimationAndSpeed(GET.pItem, 12, -1,0);

                                        // set "looking around" phase to remember that when head will be at highest head the
                                        // robot should turn to look at right.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;
                                        break;
                              }
                              return;
                    }
                    break;
          case LOOKSW_UP_AND_LEFT:
                    // we had set to move up the head and then to look at left.
                    // now we should verify if the move up movenent has been completed
                    // since when the move up with animations 9 or 10, will be completed when the next animation
                    // has been started, just checking if current animation if the current animation is:
                    // animation 10 (fixed with head at middle height)
                    // or
                    // animation 13 (fixed with highest head)
                    AnimNow = GetRelativeAnimation(GET.pItem);

                    if (AnimNow == 10) {
                              // completed: the head is at middle height. now force animation to look at left
                              // with middle height head (animation 20)
                              SetAnimationAndSpeed(GET.pItem, 20, -1,0);
                              // set new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_LEFT;
                              return;
                    }

                    if (AnimNow == 13) {
                              // completed. the head is fixed at highest position.
                              // now force animation to do look at left with highest head (animation 14)
                              SetAnimationAndSpeed(GET.pItem, 14, -1,0);
                              // se new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_LEFT;
                              return;
                    }
                    break;

          case LOOKSW_UP_AND_RIGHT:
                    // we had set to move up the head and then to look at right
                    // now we should verify if the move up movenent has been completed
                    // since when the move up with animations 9 or 10, will be completed when the next animation
                    // has been started, just checking if current animation is:
                    // animation 10 (fixed with head at middle height)
                    // or
                    // animation 13 (fixed with highest head)
                    AnimNow = GetRelativeAnimation(GET.pItem);

                    if (AnimNow == 10) {
                              // completed: the head is at middle height. now force animation to look at right
                              // with middle height head (animation 21)
                              SetAnimationAndSpeed(GET.pItem, 21, -1,0);
                              // set new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;
                              return;
                    }

                    if (AnimNow == 13) {
                              // completed. the head is fixed at highest position.
                              // now force animation to do look at right with highest head (animation 15)
                              SetAnimationAndSpeed(GET.pItem, 15, -1,0);
                              // se new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;
                              return;
                    }
                    break;
          case LOOKSW_AT_LEFT:
          case LOOKSW_AT_RIGTH:
                    // robot is turning head at left or right
                    // now verify if it has been completed.
                    // since at end of turning animation there will be one of following animations:
                    // animation 19 (after turning with lower head)
                    // animation 10 (after turning with middle head)
                    // animation 13 (after turning with highest head)
                    // just check if it has been reached one of above animations
                    AnimNow = GetRelativeAnimation(GET.pItem);
                    // save the current status to remember if it was at left or at right
                    OldStatus = GET.pItem->Reserved_34;

                    switch (AnimNow) {
                    case 19:
                              // completed with lower head
                              // we cam immediatly close the "looking around phase" and come back to common movements
                              GET.pItem->Reserved_34 = LOOKSW_DISABLED;
                              break;
                    case 10:
                              // completed with midle head.
                              // before complete we have to move down the head
                              // we use animation 11 (from middle height to lower head)
                              SetAnimationAndSpeed(GET.pItem, 11, -1,0);

                              break;
                    case 13:
                              // completed with higher head
                              // before complete we have to move down the head to lower position with animation 22
                              SetAnimationAndSpeed(GET.pItem, 22, -1,0);

                              break;
                    }
                    // verify if we are in the situation where check also at right setting LOOKSW_ENDED_LEFT_TRY_RIGHT value
                    if (OldStatus == LOOKSW_AT_LEFT && GET.pItem->Reserved_34 == LOOKSW_DISABLED) {
                              // it was looking at left but it has been completed.
                              // now we have to remember to look at right but only when the animation of robot will be newly that for moving forward
                              // since now there could be an animation to move down the head.
                              GET.pItem->Reserved_34 = LOOKSW_ENDED_LEFT_TRY_RIGHT;
                    }
                    if (OldStatus == LOOKSW_AT_RIGTH && GET.pItem->Reserved_34 == LOOKSW_DISABLED) {
                              // it was looking at right but now we completed the operation
                               // set newly the movement forward for robot
                              SetAnimationAndSpeed(GET.pItem,0,-1,32);
                    }
                    break;
          }

}


How to improve IsFreeWay() function to support all moveables as obstacles

Since we are showing last improvements about many functions, it's the moment to solve that homework I gave to you at end of Robot Cleaner chapter.
In IsFreeWay() fucntion, the check for moveables like obstacles worked only for pushable items but this is a boring limitation, since there are many other moveables could stop our robot.
Now we try to create a general purpose detection working in this way:

Above procedure has the advantage to work with any moveable, avoiding to us to write a long list about any kind of moveable we could detect.

We should change following code (from IsFreeWay() function):

          Find(enumFIND.ITEMS_SECTOR, -1, FLOOR.RoomIndex, -1, 0, &MyPos);

          // now we check if one of these items is a pushable item
          // note: we have to save the current GET.pItem structure (that is the cleaner robot) before
          // using Get(enumGET.ITEM) that will change that structure
          // at end we'll restore the old value for GET.pItem
          pSaveItem = GET.pItem;

          for (i=0;i<FIND.TotItems;i++) {
                    Index = FIND.VetItems[i];

                    Get(enumGET.ITEM, Index,0);

                    if (GET.pItem->SlotID >= enumSLOT.PUSHABLE_OBJECT1 && GET.pItem->SlotID <= enumSLOT.PUSHABLE_OBJECT5) {
                              // one of the item in that sector is own a pushable object: return false because the way is not free
                              // retore previous structure in GET.pItem
                              GET.pItem = pSaveItem;
                              return false;
                    }
          }
          // retore previous structure in GET.pItem
          GET.pItem = pSaveItem;          

Replacing it with following code:

          Find(enumFIND.ITEMS_SECTOR, -1, FLOOR.RoomIndex, -1, 0, &MyPos);

          // now we check if one of these items is a pushable item
          // note: we have to save the current GET.pItem structure (that is the cleaner robot) before
          // using Get(enumGET.ITEM) that will change that structure
          // at end we'll restore the old value for GET.pItem
          pSaveItem = GET.pItem;

          for (i=0;i<FIND.TotItems;i++) {
                    Index = FIND.VetItems[i];
                    // get infos about current movable
                    
                    Get(enumGET.INFO_ITEM, Index,0);
                    if (Index != ItemIndex && GET.InfoItem.TestCollisions == true) {
                              // there are collisions.
                              // if it is a killable enemy we will ignore him, because the robot will be able to kill him
                              if (GET.InfoItem.TestCreature == true) {
                                        // it is a creature, verifying also if it has been enabled
                                        Get(enumGET.ITEM, Index,0);

                                        if (TriggerActive(GET.pItem)==true && GET.InfoItem.TestSemiGod == true ) {
                                                  // it's not possible killing him.
                                                  // it's an obstacle
                                                  GET.pItem = pSaveItem;
                                                  return false;
                                        }
                              }else {
                                        // it's not a creature. Probably it is an animating of pushable or rolling ball ecc.
                                        // but it could be also a little pickup
                                        // so now we try to see if it is enough heigh to fordib the access to this sector.
                                        Get(enumGET.ITEM_COLL_BOX, Index,0);
                                        // the MinY is the higher point of this item. anyway, since the higher positions are negative values
                                        // we'll have to change the sign before comparing
                                        if ((- GET.pCollItem->MinY) >= 128) {
                                                  // it's enough heigh to be an onstacle
                                                  GET.pItem = pSaveItem;
                                                  return false;
                                        }
                              }
                    }
          }
          // retore previous structure in GET.pItem
          GET.pItem = pSaveItem;          

We used the Get() function to have better infos about the moveable we detected on next sector.

                    Get(enumGET.INFO_ITEM, Index,0);

Then we consider if this moveable is a creature or less.
When it is a creature, we'll not consider it like an obstacle when it is killable, because the robot will move on that sector killing him.
While, if it is not killable, we'll consider it like an obstacle.

                              // if it is a killable enemy we will ignore him, because the robot will be able to kill him
                              if (GET.InfoItem.TestCreature == true) {
                                        // it is a creature, verifying also if it has been enabled
                                        Get(enumGET.ITEM, Index,0);

                                        if (TriggerActive(GET.pItem)==true && GET.InfoItem.TestSemiGod == true ) {
                                                  // it's not possible killing him.
                                                  // it's an obstacle
                                                  GET.pItem = pSaveItem;
                                                  return false;
                                        }
                              }


When it is not a creature, we cann't kill it, and for this reason we'll analyse if the size (its heigh) it's enough big to be an obstacle.
To compute its size, we read its collision box and we check only its heigh (miny).
We set that when the item is lower than 128 units (half of one click) the robot will be able to pass over it.

                              else {
                                        // it's not a creature. Probably it is an animating of pushable or rolling ball ecc.
                                        // but it could be also a little pickup
                                        // so now we try to see if it is enough heigh to fordib the access to this sector.
                                        Get(enumGET.ITEM_COLL_BOX, Index,0);
                                        // the MinY is the higher point of this item. anyway, since the higher positions are negative values
                                        // we'll have to change the sign before comparing
                                        if ((- GET.pCollItem->MinY) >= 128) {
                                                  // it's enough heigh to be an onstacle
                                                  GET.pItem = pSaveItem;
                                                  return false;
                                        }
                              }

If you replace the code, and try in game, placing different items on the way of the robot, you'll see that little items (like torch, medipacks and other pickup items) will be not able to stop the robot, while other bigger items will be seen like an obstacle.

Remark: please note this first condition:

                    if (Index != ItemIndex && GET.InfoItem.TestCollisions == true) {

We consider like a possible obstacle the current analysed item if it has collisions (GET.InfoItem.TestCollisions == true) but we checked also that current found item was not the same main item we are moving (Index != ItemIndex). It's a weird situation but pratically it will happen always, because when main item (that we are moving and checking for its free way) move slowly on the floor, for a moment it could be on same sector we are analysing. So we verify that found item was not the same main item to avoid that this item stops itself.


Improving detecting mode

If you have not followed the debugging in debugger tutorial, build the project with new code and try it.
You see that now the detection of the robot works fine anyway the procedure whereby the robot is look for lara is a bit slow and, sometime, stupid.
Using the robot in a real labyrinth the operative mode is enough good, but if you use the robot in some wide zones the continue moving the head with stopping and go for each sector is too slow.
We could improve this method to inspect the space, avoiding some futile repetitions for contiguous zones.


Looking above image you see an example of the repetition I said...
The robot check looking at its left in two consecutive sectors, for each it will stop, of course.
It's not useful working in this way, because if it detected (or less) lara in previous sector, it's not probabe that some second later, in contiguous sector sometimes changed.
The problem should be yet more evident in a wide room with no obstacles, where the robot will go and stop a lot of times, looking where it had just looked just a moment ago.
To reduce this problem we could create a procedure where it will omit to look at same direction in contiguous sectors. This new method will not change so much in real labyrinth, but it will get faster the movement in wide zones.


How to remember what we did a moment ago?

We have to remember where the robot looked in previous sector.
If in previous sector it had already looked at left, it will skip the looking at left in current sector.
Same speech for right direction, of course, anyway the two directions have to be handled distinctly, since it's not a good reason to omit the looking at left in current sector only because in previous sector it looked at right.
The easy way to remember previous operation is to use a variable in structure of robot, where we'll set if the robot looked at left or at right. Then, we'll look what's happened in previous sector and so we'll understand when omit some check for looking at left or right.


The Reserved_36 field to remember previous looking operations

We have already used one of Reserved fields of robot structure, the Reserved_34 to set different phases of looking operations.
Now we could use the Reserved_36 field to remember type of looking performed in previous sector.
The matter is not so easy, because the looking operations don't happen in a single frame, but they begin with looking at left and then (it could be) the looking at right.
In this case, a trick to understand if we are yet on same sector (in spite many frames are elapsing) is to remember that, until we are in looking mode the robot will be still, with no forward movement, only in same sector.
So we can use the moment we detect the movement of robot, like the end of a sector (with its further looking operation) and the waiting for new sector when the robot will be newly at center of next sector.


Constants to remember previous looking operation

We could use suffix OLDL (for Old Looking):

// constants to remember the looked operation performed in previous sector
#define OLDL_NULL 0x0000
#define OLDL_LOW_LEFT 0x0001
#define OLDL_MIDDLE_LEFT 0x0002
#define OLDL_HIGH_LEFT 0x0004
#define OLDL_LOW_RIGHT 0x0008
#define OLDL_MIDDLE_RIGHT 0x0010
#define OLDL_HIGH_RIGHT 0x0020

The skipping will work only for two consecutive sectors with same operation, this means that looking at left with low head is different than looking at left with highest head, for instance, and in this case the next sector will be not skipped.

Note: we used values as flags (power by 2), instead by using progressive numbers (like 1,2,3,4,5,6) to signal looking operations, because it could happen there are two different looking operations in same sector. For iinstance when the robot looks at left and (from same sector) looks at right, too.
In this situation we'll have to store both operation in same variable and this is possible only using flag values.
For instance to signale that the robot looked at left and at right just using a code like:

          GET.pItem->Reserved_36 = OLDL_LOW_LEFT | OLDL_LOW_RIGHT;

Where the "|" (algenbric OR) is to past togheter the two values, each of them is only a bit value and so we can store until 16 different values in a short/word field like it is Reserved_36


The Reserved_38 field to save current looking operation

We need of another field to store current looking operation, since we cann't using the field where we saved operation of previous sector.
We could use Reserved_38 field to save the operation that we are doing in that moment on current sector, while we saved to Reserved_36 the operation of previous sector.
Everytime we use global variables (and the fields of robot structure are globals...) we need to initialize them to avoid to work on random values.
So in InitialiseRobotStarWars() function we'll initialize these two fields:

          GET.pItem->Reserved_36 = OLDL_NULL;
          GET.pItem->Reserved_38 = OLDL_NULL;


The new detection code to avoid repetitions

This is new version of RobotSW_LookAround() function:

void RobotSW_LookAround(void)
{
          int HeightType;
          int AnimNow;
          int OldStatus;
          bool TestQuitLooking;

          TestQuitLooking = false;

          // check (reading Reserved_34 field of robot structure) what phase of looking around is performing the robot
          switch (GET.pItem->Reserved_34) {
          case LOOKSW_ENDED_LEFT_TRY_RIGHT:
                    // when there is this status we'll have to check if it is possible looking at right
                    // but we have to wait that the previous looking at left has been completed.
                    // so, since after the looking at left there will be animation 19 with stateid = 6
                    // we now verify if the robot has right stateid to begin a new "looking" operation
                    if (GET.pItem->StateIdCurrent != 6) return;

                    // now we'll do the control at right but we have to remove the LOOKSW_ENDED_LEFT_TRY_RIGHT status because once we checked
                    // if there is no chance to look at right we have alredy completed that control

                    GET.pItem->Reserved_34 = LOOKSW_DISABLED;

                    // now we copy same code of "case LOOKSW_DISABLED:" but only the part to check at right:
                    // now we perform same computation for right direction

                    if (IsMissingWall(GET.pItem, 16384, &HeightType)==true) {
                              // it's possible look at right.
                              // now discover what kind of head height we should use:
                              switch (HeightType) {
                              case 0:
                                        // just lower head. We can begin immediatly to look at right

                                        // verify that we had already performed same look operation in previous sector:
                                        if (GET.pItem->Reserved_36 & OLDL_LOW_RIGHT) {
                                                  // we did: don't perform now the same
                                                  break;
                                        }

                                        SetAnimationAndSpeed(GET.pItem, 8, -1,0);


                                        // set new "looking around" phase:
                                        GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;

                                        GET.pItem->Reserved_38 |= OLDL_LOW_RIGHT;
                                        break;
                              case 1:
                                        // required middle height of head
                                        // verify that we had already performed same look operation in previous sector:
                                        if (GET.pItem->Reserved_36 & OLDL_MIDDLE_RIGHT) {
                                                  // we did: don't perform now the same
                                                  break;
                                        }
                                        // since robot now has lower head, we have to change height from lower to middle with
                                        // animation 9
                                        SetAnimationAndSpeed(GET.pItem, 9, -1,0);
                                        
                                        // set "looking around" phase to remember that when head will be at middle height the
                                        // robot should turn to look at right.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;

                                        GET.pItem->Reserved_38 |= OLDL_MIDDLE_RIGHT;
                                        break;
                              case 2:
                                        // required highest head.
                                        // verify that we had already performed same look operation in previous sector:
                                        if (GET.pItem->Reserved_36 & OLDL_HIGH_RIGHT) {
                                                  // we did: don't perform now the same
                                                  break;
                                        }
                                        // force animation 12
                                        SetAnimationAndSpeed(GET.pItem, 12, -1,0);

                                        // set "looking around" phase to remember that when head will be at highest head the
                                        // robot should turn to look at right.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;

                                        GET.pItem->Reserved_38 |= OLDL_HIGH_RIGHT;
                                        break;
                              }
                              
                    }

                    if (GET.pItem->Reserved_34 == LOOKSW_DISABLED) {
                              // it's not possible looking at right.
                              // so, now we set newly the animation 0 to move forward the robot
                              SetAnimationAndSpeed(GET.pItem, 0,-1,32);
                              TestQuitLooking=true;
                    }
                    break;
          case LOOKSW_DISABLED:
                    // no looking around in progress.
                    // now we should verify if it is possible beginning own now, the looking around...

                    // it's possible only when the robot is moving forward with lowerd head and flat feet (stateid = 0)
                    if (GET.pItem->StateIdCurrent != 0 && GET.pItem->StateIdCurrent != 1) {
                              // wrong state-id: it's not possible beginning now the looking around phase
                              // se we leave this sector but we have to remember to have passed another sector with no looking
                              TestQuitLooking=true;
                              break;
                    }
                    // the looking around could begin only when the robot it's near at middle of sector
                    // discover infos about floor of current position of robot
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);                    

                    if (GetDistanceXZ(GET.pItem->CordX, GET.pItem->CordZ,
                              FLOOR.SectorCoords.MiddleX, FLOOR.SectorCoords.MiddleZ) > 16) {
                              // the robot is not centered with sector: no possible now beginning looking around
                              return;
                    }          
                    // we reached the middle of one sector
                    if (FLOOR.SlopeType != enumSLOPE.FLAT) {
                              // only from flat sectors we can look around
                              // se we leave this sector but we have to remember to have passed another sector with no looking
                              TestQuitLooking=true;
                              break;
                    }

                    // it should be possible, but now it's necessary verifing if there are coorridors (free spaces)
                    // at left or rigth where to look

                    if (IsMissingWall(GET.pItem, -16384, &HeightType)==true) {
                              // it's possible look at left.
                              // now discover what kind of head height we should use:
                              switch (HeightType) {
                              case 0:
                                        // just lower head. We can begin immediatly to look at left
                                        // verify that we had already performed same look operation in previous sector:
                                        if (GET.pItem->Reserved_36 & OLDL_LOW_LEFT) {
                                                  // we did: don't perform now the same
                                                  break;
                                        }
                                        SetAnimationAndSpeed(GET.pItem, 7, -1,0);

                                        // set new "looking around" phase:
                                        GET.pItem->Reserved_34 = LOOKSW_AT_LEFT;

                                        GET.pItem->Reserved_38 |= OLDL_LOW_LEFT;
                                        break;
                              case 1:
                                        // required middle height of head
                                        // verify that we had already performed same look operation in previous sector:
                                        if (GET.pItem->Reserved_36 & OLDL_MIDDLE_LEFT) {
                                                  // we did: don't perform now the same
                                                  break;
                                        }
                                        // since robot now has lower head, we have to change height from lower to middle with
                                        // animation 9
                                        SetAnimationAndSpeed(GET.pItem, 9, -1,0);
                                        
                                        // set "looking around" phase to remember that when head will be at middle height the
                                        // robot should turn to look at left.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_LEFT;

                                        GET.pItem->Reserved_38 |= OLDL_MIDDLE_LEFT;
                                        break;
                              case 2:
                                        // required highest head.
                                        // verify that we had already performed same look operation in previous sector:
                                        if (GET.pItem->Reserved_36 & OLDL_HIGH_LEFT) {
                                                  // we did: don't perform now the same
                                                  break;
                                        }
                                        // force animation 12
                                        SetAnimationAndSpeed(GET.pItem, 12, -1,0);

                                        // set "looking around" phase to remember that when head will be at highest head the
                                        // robot should turn to look at left.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_LEFT;

                                        GET.pItem->Reserved_38 |= OLDL_HIGH_LEFT;
                                        break;
                              }                                                                      
                    }
                    if (GET.pItem->Reserved_34 == LOOKSW_DISABLED) {

                              // it was not possible looking at left
                              // now we perform same computation for right direction

                              if (IsMissingWall(GET.pItem, 16384, &HeightType)==true) {
                                        // it's possible look at right.
                                        // now discover what kind of head height we should use:
                                        switch (HeightType) {
                                        case 0:
                                                  // just lower head. We can begin immediatly to look at right
                                                  // verify that we had already performed same look operation in previous sector:
                                                  if (GET.pItem->Reserved_36 & OLDL_LOW_RIGHT) {
                                                            // we did: don't perform now the same
                                                            break;
                                                  }
                                                  SetAnimationAndSpeed(GET.pItem, 8, -1,0);

                                                  // set new "looking around" phase:
                                                  GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;

                                                  GET.pItem->Reserved_38 |= OLDL_LOW_RIGHT;
                                                  break;
                                        case 1:
                                                  // required middle height of head
                                                  // verify that we had already performed same look operation in previous sector:
                                                  if (GET.pItem->Reserved_36 & OLDL_MIDDLE_RIGHT) {
                                                            // we did: don't perform now the same
                                                            break;
                                                  }
                                                  // since robot now has lower head, we have to change height from lower to middle with
                                                  // animation 9
                                                  SetAnimationAndSpeed(GET.pItem, 9, -1,0);
                                                  
                                                  // set "looking around" phase to remember that when head will be at middle height the
                                                  // robot should turn to look at right.
                                                  GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;

                                                  GET.pItem->Reserved_38 |= OLDL_MIDDLE_RIGHT;
                                                  break;
                                        case 2:
                                                  // required highest head.
                                                  // verify that we had already performed same look operation in previous sector:
                                                  if (GET.pItem->Reserved_36 & OLDL_HIGH_RIGHT) {
                                                            // we did: don't perform now the same
                                                            break;
                                                  }
                                                  // force animation 12
                                                  SetAnimationAndSpeed(GET.pItem, 12, -1,0);

                                                  // set "looking around" phase to remember that when head will be at highest head the
                                                  // robot should turn to look at right.
                                                  GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;

                                                  GET.pItem->Reserved_38 |= OLDL_HIGH_RIGHT;
                                                  break;
                                        }
                                        
                              }
                    }

                    if (GET.pItem->Reserved_34 == LOOKSW_DISABLED) {

                                        // it has not been possible looking at left and neither at right: leave current sector:
                                        TestQuitLooking=true;
                              
                    }

                    break;
          case LOOKSW_UP_AND_LEFT:
                    // we had set to move up the head and then to look at left.
                    // now we should verify if the move up movenent has been completed
                    // since when the move up with animations 9 or 10, will be completed when the next animation
                    // has been started, just checking if current animation if the current animation is:
                    // animation 10 (fixed with head at middle height)
                    // or
                    // animation 13 (fixed with highest head)
                    AnimNow = GetRelativeAnimation(GET.pItem);

                    if (AnimNow == 10) {
                              // completed: the head is at middle height. now force animation to look at left
                              // with middle height head (animation 20)
                              SetAnimationAndSpeed(GET.pItem, 20, -1,0);
                              // set new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_LEFT;
                              return;
                    }

                    if (AnimNow == 13) {
                              // completed. the head is fixed at highest position.
                              // now force animation to do look at left with highest head (animation 14)
                              SetAnimationAndSpeed(GET.pItem, 14, -1,0);
                              // se new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_LEFT;
                              return;
                    }
                    break;

          case LOOKSW_UP_AND_RIGHT:
                    // we had set to move up the head and then to look at right
                    // now we should verify if the move up movenent has been completed
                    // since when the move up with animations 9 or 10, will be completed when the next animation
                    // has been started, just checking if current animation is:
                    // animation 10 (fixed with head at middle height)
                    // or
                    // animation 13 (fixed with highest head)
                    AnimNow = GetRelativeAnimation(GET.pItem);

                    if (AnimNow == 10) {
                              // completed: the head is at middle height. now force animation to look at right
                              // with middle height head (animation 21)
                              SetAnimationAndSpeed(GET.pItem, 21, -1,0);
                              // set new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;
                              return;
                    }

                    if (AnimNow == 13) {
                              // completed. the head is fixed at highest position.
                              // now force animation to do look at right with highest head (animation 15)
                              SetAnimationAndSpeed(GET.pItem, 15, -1,0);
                              // se new phase
                              GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;
                              return;
                    }
                    break;
          case LOOKSW_AT_LEFT:
          case LOOKSW_AT_RIGTH:
                    // robot is turning head at left or right
                    // now verify if it has been completed.
                    // since at end of turning animation there will be one of following animations:
                    // animation 19 (after turning with lower head)
                    // animation 10 (after turning with middle head)
                    // animation 13 (after turning with highest head)
                    // just check if it has been reached one of above animations
                    AnimNow = GetRelativeAnimation(GET.pItem);
                    // save the current status to remember if it was at left or at right
                    OldStatus = GET.pItem->Reserved_34;

                    switch (AnimNow) {
                    case 19:
                              // completed with lower head
                              // we cam immediatly close the "looking around phase" and come back to common movements
                              GET.pItem->Reserved_34 = LOOKSW_DISABLED;
                              break;
                    case 10:
                              // completed with midle head.
                              // before complete we have to move down the head
                              // we use animation 11 (from middle height to lower head)
                              SetAnimationAndSpeed(GET.pItem, 11, -1,0);

                              break;
                    case 13:
                              // completed with higher head
                              // before complete we have to move down the head to lower position with animation 22
                              SetAnimationAndSpeed(GET.pItem, 22, -1,0);

                              break;
                    }
                    // verify if we are in the situation where check also at right setting LOOKSW_ENDED_LEFT_TRY_RIGHT value
                    if (OldStatus == LOOKSW_AT_LEFT && GET.pItem->Reserved_34 == LOOKSW_DISABLED) {
                              // it was looking at left but it has been completed.
                              // now we have to remember to look at right but only when the animation of robot will be newly that for moving forward
                              // since now there could be an animation to move down the head.
                              GET.pItem->Reserved_34 = LOOKSW_ENDED_LEFT_TRY_RIGHT;
                    }

                    if (OldStatus == LOOKSW_AT_RIGTH && GET.pItem->Reserved_34 == LOOKSW_DISABLED) {
                              // it was looking at right but now we completed the operation
                               // set newly the movement forward for robot
                              SetAnimationAndSpeed(GET.pItem,0,-1,32);
                              TestQuitLooking=true;
                    }
                    break;
          }

          if (TestQuitLooking==true) {
                    // We have just now completed the looking operations: now we have to copy the info for
                    // current sector (Reserved_38) to that for previous sector (Reserved_36) and
                    // clear the data for current sector that it will become next sector that we'll meet in future

                    GET.pItem->Reserved_36 = GET.pItem->Reserved_38;
                    GET.pItem->Reserved_38 = OLDL_NULL;

          }
}



What's the news in RobotSW_LookAround() function?

We create this new local variable:

          bool TestQuitLooking;

That's very important for our computations, because we'll set "true" in this variable when we just completed the looking around operation on current sector.
Then, at end of RobotSW_LookAround() function, we'll check if TestQuitLooking == true and it it's, we'll move the value from Reserved_38 (current sector) to Reserved_36 (previous sector) and then we clear the Reserved_38 to host future operations in next sector we'll find.

          if (TestQuitLooking==true) {
                    // We have just now completed the looking operations: now we have to copy the info for
                    // current sector (Reserved_38) to that for previous sector (Reserved_36) and
                    // clear the data for current sector that it will become next sector that we'll meet in future

                    GET.pItem->Reserved_36 = GET.pItem->Reserved_38;
                    GET.pItem->Reserved_38 = OLDL_NULL;
          }


We consider as already performed the looking operations on current sector in following cases:
  1. It's not possible looking at left or right but we are at center of current sector

  2. We looked at left but it's not possible look at right

  3. We completed the looking at right

  4. We are on sloped floor and for sure we cann't look anywhere



The Reserved_36 field
In Reserved_36 field there will be infos about the looking operations we performed in previous sector.
So we'll test the value in Reserved_36 to verify if the operation that we are going to perform it's the same we had already performed in previous sector.
If it is the same, the we'll skip to perform it now.
For instance, about verifying to look at right we find this code:

                    // now we perform same computation for right direction

                    if (IsMissingWall(GET.pItem, 16384, &HeightType)==true) {
                              // it's possible look at right.
                              // now discover what kind of head height we should use:
                              switch (HeightType) {
                              case 0:
                                        // just lower head. We can begin immediatly to look at right

                                        // verify that we had already performed same look operation in previous sector:
                                        if (GET.pItem->Reserved_36 & OLDL_LOW_RIGHT) {
                                                  // we did: don't perform now the same
                                                  break;
                                        }

                                        SetAnimationAndSpeed(GET.pItem, 8, -1,0);


                                        // set new "looking around" phase:
                                        GET.pItem->Reserved_34 = LOOKSW_AT_RIGTH;

                                        GET.pItem->Reserved_38 |= OLDL_LOW_RIGHT;
                                        break;

Once we discover to have already performed same operation of previous sector:

                                        if (GET.pItem->Reserved_36 & OLDL_LOW_RIGHT) {

We quit like it was not possible performing that looking operation:

                                                  // we did: don't perform now the same
                                                  break;
                                        }


The Reserved_38 field
In Reserved_38 field we save the operation we are performing in current sector.
Then, when we'll leave this sector, the "current" sector it will become the "previous" sector, and so we'll copy the value from Reserved_38 to Reserved_36.
In the RobotSW_LookAround() function, everytime we perform a looking operation we have to save the right value in Reserved_38 field, remembering to use the "|" (or) operator, to add current flag to further other flags, since it's possible, for instance, that we are looking at right, but a moment ago, from same sector, we looked at left and we have to preserve the OLDL_LOW_LEFT flag while we are adding the other OLDL_LOW_RIGHT flag.
For instance after having begun the looking at left operation, with highest head, we add also the OLDL... flag to Reserved_38 field to remember that operation:

                              case 2:
                                        // required highest head.
                                        // verify that we had already performed same look operation in previous sector:
                                        if (GET.pItem->Reserved_36 & OLDL_HIGH_RIGHT) {
                                                  // we did: don't perform now the same
                                                  break;
                                        }
                                        // force animation 12
                                        SetAnimationAndSpeed(GET.pItem, 12, -1,0);

                                        // set "looking around" phase to remember that when head will be at highest head the
                                        // robot should turn to look at right.
                                        GET.pItem->Reserved_34 = LOOKSW_UP_AND_RIGHT;

                                        GET.pItem->Reserved_38 |= OLDL_HIGH_RIGHT;
                                        break;

Replace the code with new version and try in game this new release.

Other improvements for Star Wars Robot

The new detection works fine and there is a smarter inspection method now, anyway now we have to add some settings that level builder will be able to change to remove the inspection mode if they wish.


The new SWR_DISABLE_INSPECTION flag
It could seem weird, after all this job, give the chance to disable it, but when you create some feature you should thing that someone could prefer aovid it, at least in some situation.
Since the inspection mode, looking at left and right, get slower the movements of robot, we could add a new flag for customize=CUST_STAR_WARS_ROBOT command to disable the inspection mode.
We could name it: SWR_DISABLE_INSPECTION

#define SWR_DISABLE_INSPECTION 0x0008

We'll have to add this value also in our .script file, of course:

SWR_DISABLE_INSPECTION:$0008 ;Used with Customize=CUST_STAR_WARS_ROBOT command>By default the robot, when it has been enabled at least one flags between SWR_SUPERVISORY, SWR_HURTING or SWR_KILLING flag, will try to look for lara, moving the head, turning it or moving up to reach higher wall (upto one sector).>Anyway, since with this inspection mode the robot is a bit slow, since it will stop everytime it has to look around, you can disable this skill adding the SWR_DISABLE_INSPECTION flag to Flags field of Customize=CUST_STAR_WARS_ROBOT command.

The code to handle this flag in our sources is very easy.
Just replacing this code (that you find in ControlRobotStarWars() function)

          // If robot is in supervisory, killing or hurting mode, discover if it is able to see lara in this moment:
          if (Flags & (SWR_SUPERVISORY | SWR_HURTING | SWR_KILLING)) {
                    // yes, some mode that requires dedection skills of the robot has been enabled.
                    // now we check if robot is able to see lara.
                    RobotSW_LookAround();
                    RobotSW_DetectAndAttack(Flags);
          }

With this:

          // If robot is in supervisory, killing or hurting mode, discover if it is able to see lara in this moment:
          if (Flags & (SWR_SUPERVISORY | SWR_HURTING | SWR_KILLING)) {
                    // yes, some mode that requires dedection skills of the robot has been enabled.
                    // now we check if robot is able to see lara.
                    // we perform looking around only if it has not been disable
                    if ((Flags & SWR_DISABLE_INSPECTION)==0) {
                              RobotSW_LookAround();
                    }
                    RobotSW_DetectAndAttack(Flags);
          }

We change only the call to RobotSW_LookAround() function, performing it only if the SWR_DISABLE_INSPECTION flag is missing:

                    if ((Flags & SWR_DISABLE_INSPECTION)==0) {
                              RobotSW_LookAround();
                    }


Testing the SWR_DISABLE_INSPECTION flag
Now quit (if it was running) NG_Center program, and the launch it newly.
This operation it's necessary to get the loading of updated .script file.
Now in the Customize=CUST_STAR_WARS_ROBOT command. we add the new SWR_DISABLE_INSPECTION flag.

Customize=                    CUST_STAR_WARS_ROBOT, 88, SWR_SUPERVISORY + SWR_DISABLE_INSPECTION, IGNORE, 1

Build the script.
Now we have also to build our plugin sources and update plugin_...dll to trle folder.
Once we performed above operations, we can launch tomb4 to verify if now the robot moves aoviding to inspect the level with looking around feature.

It works fine.
In this way it will be possible having some SW robot with inspection mode and others without it but with faster moving.


Adding a shadow for the robot

Since the robot has two (almost) legs and its body is over the floor, it should be better enabling the shadow on the floor like for other enemies.
Adding the shadow it's easy: just typing a value different than 0 in FootStep field of its slot structure.
We should perform this operation when we were initializing its slot, and so we add to InitSlotRobotStarWars() function this code:

          GET.pSlot->FootStep = 256;

The value we set in FootStep is the radius of shadow elipse. Since the robot has a diameter of about half sector, 256 as radius should be fine.
Now try in game...

Shadown doesn't work fine



There is a problem...
While at beginning the shadow works fine and it has right size, once the robot move up the shadow disappear.
It seems that only when the robot is on lower floor the shadow works.
I believe to know the reason.
We have forgotten to update the HeightFloor field of robot structure when we change it's coordinate.
The HeightFloor should keep always the current height of the floor where that object stands.
So to fix this bug we have to add to the code of RobotSW_MoveAndSteer() the instruction to update the HeightFloor:

          // update Y coordinate and room of robot
          GET.pItem->CordY = FLOOR.FloorHeight;

          GET.pItem->HeightFloor = FLOOR.FloorHeight;

We have added (after the updating of CordY field, the updating of HeightFloor, using the same value: the height of current floor.
Now we try in game with this new fixing.




Ok, now it works with any height of floor.

Adding injuring on touch

We have not set any damage for lara when she touches the robot.
We should add also another flag to give to the level builder the chance to enable or less the injuring on touching.
So, we add this constant to "Constants_mine.h" source:

#define SWR_INJURING_ON_TOUCH 0x0010

And the same constant we have to add to the our .script file:

SWR_INJURING_ON_TOUCH:$0010 ;Used with Customize=CUST_STAR_WARS_ROBOT command>Adding this flag the robot will injuries lara when it will touch her

Remember that, everytime you change the .script file, you need to quit ng_center and then launch it newly to do load the updated version of .script file.


The code to injury lara on touch
The code to handle the injuring should be typed in CollisionRobotStarWars() function, of course.

void CollisionRobotStarWars(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision)
{
          Get(enumGET.ITEM, ItemIndex, 0);


          if (TriggerActive(GET.pItem)== false) return;

          if (GetMaxDistance(&GET.pItem->CordX, &pLara->CordX, false) > 1024) return;

          if (TestBoundCollide(GET.pItem, pLara, pLaraCollision->LaraSizeX)==false) return;

          if (TestCollision(GET.pItem, pLara)==false) return;

          // lara and robot are in collision
          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);

}

We have to read the flags from Cutomize command and then check if the SWR_INJURING_ON_TOUCH flag is present.
When it is present, we'll reduce the health of lara and we'll set the bit 0x10 used to show hp bar on the screen.
This is the code:

void CollisionRobotStarWars(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision)
{
          DWORD Flags;

          Get(enumGET.ITEM, ItemIndex, 0);

          if (TriggerActive(GET.pItem)== false) return;

          if (GetMaxDistance(&GET.pItem->CordX, &pLara->CordX, false) > 1024) return;

          if (TestBoundCollide(GET.pItem, pLara, pLaraCollision->LaraSizeX)==false) return;

          if (TestCollision(GET.pItem, pLara)==false) return;

          // lara and robot are in collision
          // read setting from customize command
          Flags = RobotSW_ReadSettings(ItemIndex);
          if (Flags & SWR_INJURING_ON_TOUCH) {
                    // injury lara
                    pLara->Health -= 40;
                    if (pLara->Health < 0) pLara->Health=0;
                    pLara->FlagsMain |= 0x10;
          }
          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);
}


Those weird error messages about "identifier not found"
If you try to build the project (F7 key) with above new code, you'll receve this weird error message:

error C3861: 'RobotSW_ReadSettings': identifier not found

Above error is weird, because the "RobotSW_ReadSettings" identifier exists...
In spite I've already explained this kind of error in Basics of C++ Language help, I wish repeat here the reason.
Everytime you use an identifier (name of function or variable) it's necessary that indentier had been defined in previous rows of source, otherwise for Visual Express it's missing, in spite it existed but in next rows of the source.
In our case, we used (calling the function) the RobotSW_ReadSettings() function: it exists, but the body of this function is placed after the CollisionRobotStarWars() function where we typed the row:

          Flags = RobotSW_ReadSettings(ItemIndex);

So, to fix the compiler error, we have two methods:
  1. Perform a copy (really a CUT) and paste, to move whole RobotSW_ReadSettings() function first of the function from where we are calling it.
    So in this case we should select all body of RobotSW_ReadSettings() function, choose "cut" from edit menu, and them move over (and outside) of CollisionRobotStarWars() function, and select "Paste" from edit menu.

  2. Declare at (*)top of current source the prototype of RobotSW_ReadSettings() function, in this way Visual Express will remember that function exists and what are its arguments.
    The prototype is first row, where we find the name of the fuction, its arguments and the returned value, followed by a ";" sign.
    In our case the prototype is:
    DWORD RobotSW_ReadSettings(int ItemIndex);

    (*) Note: really the position where you should place the prototype it should be after the rows with "#include" directive but first of first body of first function in the source.

Now follow one of these two methods to solve the problem and then build the project and update the plugin_...dll library in trle folder.
Then, in ng_center add the new flag to Customize command for robot, and build the script.


It works, perhaps a bit too much...


It works but the value we used (40) is too strong, because it will be substracted from health 30 times for second.
So we could reduce it a bit: for instance using 20 as damage.


How to get settings from Enemy script command

In spite we have already a cutomize for our robot, we could accept also the settings from Enemy script command, in paritcular way that about the damage level when an enemy injures lara.
We could suppose that in the script there was an enemy command like this:

Enemy= ROBOT_STAR_WARS, IGNORE, IGNORE, IGNORE, IGNORE, 30

The last field, with value 30, is Damage1 field.
If there is an enemy command for SW robot, we could verify if it has been set a valid value (different than -1) and in that case, to use that value to set the injury level when the robot touch lara.
While, in the case the Enemy command was missing, or it has not been set a valid value for Damage1, we'll use our default of damage (20)
This is way to link our new object with old trng commands.
So we'll change the collision code in this way:


          // lara and robot are in collision
          // read setting from customize command
          Flags = RobotSW_ReadSettings(ItemIndex);
          if (Flags & SWR_INJURING_ON_TOUCH) {
                    // injury lara
                    Damage1= 20; // our default value

                    // discover if there is a setting in Enemy command for robot
                    if (Find(enumFIND.ENEMY_COMMAND, 499, -1, -1, -1, NULL)== true) {
                              if (FIND.pEnemy->TotDamage > 0) {
                                        if (FIND.pEnemy->VetDamage[0] != -1) {
                                                  // there is a valid value for damag1
                                                  Damage1= FIND.pEnemy->VetDamage[0];
                                        }
                              }
                    }                    
                    
                    pLara->Health -= Damage1;
                    if (pLara->Health < 0) pLara->Health=0;
                    pLara->FlagsMain |= 0x10;
          }

We changed the value to substract with a local variable "Damage1".
Then we have a serie of conditions:

                    if (Find(enumFIND.ENEMY_COMMAND, 499, -1, -1, -1, NULL)== true) {
                              if (FIND.pEnemy->TotDamage > 0) {
                                        if (FIND.pEnemy->VetDamage[0] != -1) {
                                                  // there is a valid value for damag1
                                                  Damage1= FIND.pEnemy->VetDamage[0];
                                        }
                              }
                    }          

First we looked for a Enemy command with slot 499 (the slot id for Star Wars Robot)

if (Find(enumFIND.ENEMY_COMMAND, 499, -1, -1, -1, NULL)== true) {

We checked if the Find() function returns true, otherwise the enemy is missing.
Then we checked if in the enemy command has been typed at least one damage field, testing the "TotDamage" field, where there is the number of damage (0,1,2 or 3) that the level builder set.

if (FIND.pEnemy->TotDamage > 0) {

But now we have also to verify that the value was not IGNORE (i.e. "-1") because it could be possible that level builder set damage1 as IGNORE only to type a valid value for Damage2, for instance.

if (FIND.pEnemy->VetDamage[0] != -1) {

Only when all above condition were true, we can read the value of damage1, from first cell of VetDamage[] vector, i.e. from cell with index = 0:

Damage1= FIND.pEnemy->VetDamage[0];


Now build the project, update plugin_...dll in trle folder, and then try to add to the script, an Enemy command like this:

Enemy= ROBOT_STAR_WARS, IGNORE, IGNORE, IGNORE, IGNORE, 1

Then build the script and try the game.
If it works, the damage of lara will be very little (1), while first, without the Enemy command, was bigger.
Another test is setting Damage1 with value 1000: in this case, lara should be killed at first touch of the robot.


How to shoot electrical lightnings

Now we have to make the code for SWR_HURTING and SWR_KILLING settings.
In both cases we need to shoot a lightning to hit lara when she has been detected from the robot.


The TriggerLightning() function
To draw a lightning we can use the tomb4 TriggerLightning() function.
Its arguments are:



Why should we reinvent the wheel?

The TriggerLightning() function is really a crabby function.
Just wronging a bit some parameter to get terrible results.
Since it's already present a flipeffect to shoot lightning with any setting, we could use it.

; Set Trigger Type - FLIPEFFECT 359
; Exporting: TRIGGER(1025:0) for FLIPEFFECT(359) {Tomb_NextGeneration}
; <#> : Weather. Perform a lightning with data in <&>Parameters for (E)Durate in Tick frames (1/30 second)
; <&> : Parameters=PARAM_LIGHTNING, 1
; (E) : Frame Ticks= 4
; Values to add in script command: $2000, 359, $401


How to handle triggers requiring Param commands in the script

We know how to perform some trng trigger.
We can use a direct function, like PerformFlipeffect() function, or to call a generic PerformExportedTrigger() function, that works with all triggers suppling to it the three numbers of exported triggers.
Anyway in the case of flipeffect 359 we discover to have a problem:

; <#> : Weather. Perform a lightning with data in <&>Parameters for (E)Durate in Tick frames (1/30 second)

This trigger requires a script command (Parameters=PARAM_LIGHTNING) to receive all settings.

Now it's important understanding well this speech...

If we are building a plugin only for our own exclusive usage, then we can type in the script some PARAM_LIGHTNING script command, remembering its ID, and then call the PerformFlipeffect() function with right Id and solve easily the problem...

But... when we are building a plugin to share it with many level builders, it should be better that the code in the plugin was able to work in independent way, without the need to give to our final users a long list about all script commands to add to the script (and own with those IDs) to do work, Star Wars Robot and other features.
Pratically, in this situation we should create a code that was able to work in stand-alone way, with all required stuff, enclosed in the plugin library.

How to create new script commands

Since from your plugin you can call all triggers built in tomb_nextgeneration library, to solve the problem about trng trigger requiring some Param scritp commands, it has been created a method to create dynamically a Parameters=PARAM_... script command.
When you create a script command dynamically, it's like the game had read that command from the script, in spite it has been your code to "write" that script command.
To create a Parameters= script command you have to use the function:

Service(enumSRV.CREATE_PARAM_COMMAND, ...);

The parameters you have to type inside of the "...", are the same arguments you should type in a Parameters= script command with only a pair of exceptions:
You have not to supply an ID for the script commands created dynamically, because you cann't know if in the real script there is already a script command with that same ID you chose.
To avoid this problem, it will be the Service() function to assign to your script command a new id, different by all others, and it will return to you own that value: the id assigned to this new script command you have just created.
For instance, if we need to create a parameters= command for lightning, we have to see the syntax in ng_center for this script command:

Syntax: Parameters=PARAM_LIGHTNING, IdParamList, Lightning flags (LGTN_...), SourcePosItem, TargetPosItem, IdColorRGB, Intensity, SoundEffect, Size, ParticleDurate, IntervalTime, Alfa, Beta

So we'll supply all arguments required for this command, but skipping the "IdParamList" argument, that it will be returned by the Service() function.
Another news is that at begin of argument list, we'll type a "true" or "false" to set if this script command will be removed bysefl after its usage or less.

About the meaning of this "true" or "false" at begin and about the reason to delete the script commands we created you should read How to create dynamically Script Commands help document.



Now we can compare the arguments required from the PARAM_LIGHTNING command, looking the syntax of common script command (read in NG_Center - Reference Panel) and the real list of argument we give to create dynamically the same command.
Syntax