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 we read in NG_Center for PARAM_LIGHTNING command

Syntax: Parameters=PARAM_LIGHTNING, IdParamList, Lightning flags (LGTN_...), SourcePosItem, TargetPosItem, IdColorRGB, Intensity, SoundEffect, Size, ParticleDurate, IntervalTime, Alfa, Beta

We'll use same argument lists but toggling the "IdParamList" argument, and adding as first argument a "true" or "false" to require that the command will be deleted after its first usage ("true") or less ("false"):


          int IdParam;

          IdParam = Service(enumSRV.CREATE_PARAM_COMMAND, true, PARAM_LIGHTNING, LightningFlags, SourcePosItem, TargetPosItem, IdColorRGB, Intensity, SoundEffect, Size, ParticleDurate, IntervalTime, Alfa, Beta, END_LIST);

Everytime we create a script command with a variable number of arguments, we'll have to complete the argument list with the END_LIST constant.
It happens always with CREATE_PARAM_COMMAND command because there are different PARAM commands with different number of arguments.

Looking the parameter list, we see that it will be required also an IdColorRgb to set a color.
We can create also a ColorRgb= command and we'll have to create first the colorrgb, so to get its id, and then we'll create the Parameters command, passing also the IdColorRgb we had just created.
About the flags, the same of LGTN_ flags we see in NG_center, we'll set different flags in according with type of lightning we wish get.
Since the robot flags could require the killing (SWR_KILLING) or only the injuring of lara (SWR_HURTING), we'll change the flags for lightning to kill or less lara in according with these SWR_ flags.
Supposing to use a blue color to injuring while a red color to kill, we could use these settings for Parameters= command:

Parameters=          PARAM_LIGHTNING, LGTN_flags, OTYPE_MOVEABLE+RobotIndex, OTYPE_MOVEABLE_STATIC+LaraIndex, IdColor, 32, 198, 16, 16, IGNORE, 1, 5

We'll type the code to shoot the lightning in a new distinct function, for instance the ShootLightning() function

void ShootLightning(int SourceItemIndex, bool TestKill)
{
          int IdColor;
          WORD Flags;
          int IdParam;

          Flags=LGTN_PLAY_SOUND+LGTN_ADD_GLOVE_LIGHT;

          if (TestKill==true) {
                    // red color
                    IdColor= Service(enumSRV.CREATE_COLOR_RGB_COMMAND, true, 128,0,0);
                    // flags to kill lara and to do scream her
                    Flags |= LGTN_FIRE_LARA;
                    Flags |= LGTN_LARA_SCREAM;
                    Flags |= LGTN_KILL_TARGET;
          }else {
                    // don't kill but only inury lara
                    IdColor= Service(enumSRV.CREATE_COLOR_RGB_COMMAND, true, 0,0,128);
                    GET.pLara->Health -= 100;
                    GET.pLara->FlagsMain |= 0x10;
          }
          IdParam=Service(enumSRV.CREATE_PARAM_COMMAND,
                                        true,                               // Testdynamic = true
                                        PARAM_LIGHTNING, // type of parameter: PARAM_LIGHTNING
                                        Flags,                               // Flags LGTN
                                        FromTomb4IndexToNgleIndex(SourceItemIndex), // Source item: index of star wars robot
                                        FromTomb4IndexToNgleIndex(GET.LaraIndex), // Target item: index of lara
                                        IdColor, // Id of ColorRbg
                                        32,                              // Intensity (default 32)
                                        198,                              // sound effect (default 198 ELEC_ONE_SHOT)
                                        16,                              // Size (default 16)
                                        16,                              // Particle durate (default 24)
                                        -1,                              // IntervalTime (default -1 continue)
                                        1,                              // Alfa (default 1)
                                        5,                              // Beta (default 5)
                                        END_LIST);                    // END_LIST (required for each CREATE_PARAM_COMMAND service)
          // call the trigger with a durate of 3 frames          
          PerformFlipeffect(NULL, 359, IdParam, 3);
}

Looking above code we can note some things:
We have two input parameters...

int SourceItemIndex, bool TestKill

The SourceItemIndex will be the index of Star Wars robot that we'll use as source position for the lightning.
While TestKill will be used to set if the lightning will kill immediately lara (TestKill== true) or less.
In according with the value of TestKill we'll set different LGTN_ flags and color...

          if (TestKill==true) {
                    // red color
                    IdColor= Service(enumSRV.CREATE_COLOR_RGB_COMMAND, true, 255,0,0);
                    // flags to kill lara and to do scream her
                    Flags |= LGTN_FIRE_LARA;
                    Flags |= LGTN_LARA_SCREAM;
                    Flags |= LGTN_KILL_TARGET;
          }else {
                    // don't kill but only inury lara
                    IdColor= Service(enumSRV.CREATE_COLOR_RGB_COMMAND, true, 0,0,255);
                    GET.pLara->Health -= 100;
                    GET.pLara->FlagsMain |= 0x10;
          }

When it required immediate killing, we'll use the LGTN_ flags to fire, scream and kill lara.
For killing we'll use red color for the ligthning.
While with TestKill == false, we'll use blue color for lightning, and we'll toggle to lara some health (100 in above code)

Once we create ColorRgb and Parameters=PARAM_LIGHTNING script commands, we can call the flipeffect 359 passing to it the ID of PARAM_LIGHTNING command to perform the lightning.

          // call the trigger with a durate of 3 frames          
          PerformFlipeffect(NULL, 359, IdParam, 3);

Please, note that we created both script commands with first parameter = true.
This means that both commands will be deleted by trng engine when the lightning has been completed.
In this way we'll avoid to waste script commands resources.

We should note also another issue: when we set the source and target position based on indices of moveable items, we passed those arguments in this way:

                                        FromTomb4IndexToNgleIndex(SourceItemIndex), // Source item: index of star wars robot
                                        FromTomb4IndexToNgleIndex(GET.LaraIndex), // Target item: index of lara

The SourceItemIndex will be the index of Star Wars robot, while GET.LaraIndex is the index of Lara, of course.
Anyway we see that we have converted those indices with FromTomb4IndexToNgleIndex() function.
The reason is that we are building a script command, and when level builder type a moveable index in some script command, it can use ONLY ngle indices, because it's not possible to know in advance the tomb4 index.
This means that we have to do the opposite procedure: since we have only "native" tomb4 indices, when you type these indices in some script command, we have to convert them to ngle format because is that the format used in script commands.
The FromTomb4IndexToNgleIndex() function has own this target. You give as input argument a tomb4 moveable index, and the function will return the NGLE index for that moveable.


The code to call the ShootLightning() fucntion

Now we have also to call the ShootLightning() function in right moment.
It will happen when robot detected lara and the setting in customize=CUST_STAR_WARS_ROBOT command will have SWR_HURTING or SWR_KILLING flags.
We'll type this code in RobotSW_DetectAndAttack() function, of course.

          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]);
                    }else {
                              // it's missing SWR_SUPERVISORY flag
                              // if there is hurting or killing shooting the lightning
                              // anyway, if it elasped too few time from last shooting: skip other shooting
                              if (GET.pItem->Reserved_3A == 0) {
                                        if (Flags & SWR_HURTING) {
                                                  ShootLightning(RobotIndex, false);
                                        }
                                        if (Flags & SWR_KILLING) {
                                                  ShootLightning(RobotIndex, true);
                                        }
                                        // begin a new delay before next shooting
                                        GET.pItem->Reserved_3A = 60;
                              }else {
                                        // we are in delay time, anyway force the view of health bar of lara since she has been just hit
                                        GET.pLara->FlagsMain |= 0x10;
                                        // if it elapsed first second from last shooting to do also sound effect to do moan lara
                                        // if it is only injury (and she is not yet dead)
                                        if ((Flags & SWR_HURTING) != 0 &&

                                                  GET.pLara->Health > 0) {
                                                  if (GET.pItem->Reserved_3A > 50) {
                                                            SoundEffect(31, &GET.pLara->CordX, 0);
                                                  }
                                        }
                              }
                    }
          }

Above code begins with the CheckDirection() function, used to verify if robot is able to see lara.
When it is true, we'll check the SWR_ flags to see what it should happen...
If it has been enabled the SWR_SUPERVISORY flag, we'll perform the triggergroup ID set in Customize command:

                    if (Flags & SWR_SUPERVISORY) {
                              PerformTriggerGroup(GET.pCust->pVetArg[3]);

While, "else", if it's not present, we'll check for SWR_HURTING or SWR_KILLING flags...
In both cases we'll call the ShootLightning() function, changing only the value for TestKill:

                                        if (Flags & SWR_HURTING) {
                                                  ShootLightning(RobotIndex, false);
                                        }
                                        if (Flags & SWR_KILLING) {
                                                  ShootLightning(RobotIndex, true);
                                        }

We see that we have another reserved variable now: the "Reserved_3A" field of robot structure, but what is its target?!

How to handle delay times

There will be a lot of situations where we wish temporize some actions: after have performed an action, waiting a moment before peforming a new action.
For instance when a baddy shoots with a weapon, after that shooting there will be a pause to simulate the reloading of the weapon.
Also for lightning shooting is advisable to do a pause between a shooting and the next, also because a lightning has some durate and shooting continuosly we could overlap two or more lightnings in same moment.
To handle in the code this pause we have to use a frame counter, where we'll save a number of frame to wait, then we'll decrease by 1 that counter for each frame, and then we'll check when that counter reached 0.
Only when the counter reached 0 we'll perform the next action.
We used the "Reserved_3A" field of robot structure as frame counter to handle the reloading time betwen a shooting and the next.
Once we have just shot a lightning we'll type a value of 60 frames (about 2 seconds) in "Reserved_3A" field:

                                        if (Flags & SWR_HURTING) {
                                                  ShootLightning(RobotIndex, false);
                                        }

                                        if (Flags & SWR_KILLING) {
                                                  ShootLightning(RobotIndex, true);
                                        }
                                        // begin a new delay before next shooting
                                        GET.pItem->Reserved_3A = 60;

By other hand, in same code but a row first, we'll check if delay counter is 0 to shoot a lightning, because if it isn't this means that we are yet in reloading time and we'll have to skip the shooting:

                              // if there is hurting or killing shooting the lightning
                              // anyway, if it elasped too few time from last shooting: skip other shooting
                              if (GET.pItem->Reserved_3A == 0) {
                                        
                                        if (Flags & SWR_HURTING) {
                                                  ShootLightning(RobotIndex, false);
                                        }

                                        if (Flags & SWR_KILLING) {
                                                  ShootLightning(RobotIndex, true);
                                        }

We should remember also to decrease by 1 our frame counter for each game cycle.
We could place this code in many different positions, anyway we'll use the first rows of ControlRobotStarWars() function, but only when the robot has been already triggered:

void ControlRobotStarWars(short ItemIndex)
{
          DWORD Flags;

          Get(enumGET.ITEM, ItemIndex,0);

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

          // decrease countdown for shooting interval (at least it was not already 0)
          if (GET.pItem->Reserved_3A != 0) {
                    GET.pItem->Reserved_3A--;
          }

In above code we see that we, before decreasing the counter, we check if it has not already reched the 0, because in that case we should avoid to decrease it newly, otherwise it will become a negative value.

Everytime we use a global variable we should remember to initialise it, first to use it...
So we could initialize the Reserved_3A field in InitialiseRobotStarWars() function:

          // initialise "looking around" mode to 0 (disabled) (constants with LOOKSW_ prefix)
          GET.pItem->Reserved_34 = LOOKSW_DISABLED;
          GET.pItem->Reserved_36 = OLDL_NULL;
          GET.pItem->Reserved_38 = OLDL_NULL;
          GET.pItem->Reserved_3A = 0; // countdown to shoot


How to handle looped sound effects

Coming back to our code in RobotSW_DetectAndAttack() function, where we detected lara, we see that there is an extra code when we are in delay time, i.e. when the Reserved_3A variable is different than 0:

                              }else {
                                        // we are in delay time, anyway force the view of health bar of lara since she has been just hit
                                        GET.pLara->FlagsMain |= 0x10;
                                        // if it elapsed first second from last shooting to do also sound effect to do moan lara
                                        // if it is only injury (and she is not yet dead)
                                        if ((Flags & SWR_HURTING) != 0 &&
                                                  GET.pLara->Health > 0) {
                                                  if (GET.pItem->Reserved_3A > 50) {
                                                            SoundEffect(31, &GET.pLara->CordX, 0);
                                                  }
                                        }
                              }

In above code we are using the dealy time to do some stuff:
- We add flag 0x10 to FlagsMain of lara to force the drawing of health bar. indeed if there is a dealy time it means that lara had just been shotn by robot.
- When there is the SWR_HURTING flag and lara is not already dead, we perform the sound effect 31 LARA_INJURY, to do lara "moans" after she has been just hit.

Why place here these two operations? Why cann't we place in the ShootLightning() function, when we shot the lightning?
Because both operations requires to be performed continuosly to work fine.
The 0x10 flag indeed, show the health bar only when it is present in current frame, but in next frame it will be removed from tomb raider engine itself.
While the sound effect 31 is a multiple sound effect with two sub-sounds "takehit1" and "takehit2".
These weird multiple sound effect, like those "looped", require to be called continuosly, otherwise they will not work.
Since the shooting of lightning it will happen only in a single frame, and then we'll do a pause, performing that sound in that single frame should do not work it.
So we detected to verify if we are in delay time, and in that case we'll play continuosly that sound.
We check the value in Reserved_3A variable, to avoid to do moan lara for two seconds.

                                                  if (GET.pItem->Reserved_3A > 50) {
                                                            SoundEffect(31, &GET.pLara->CordX, 0);
                                                  }

We check if the value is grater than 50, and only in this case we'll play the sound.
Since the frame counter will start by 60 and will be decreased until 0, we'll play the sound in first 10 frames after the shooting: at 60, 59,58, 57, 56, 55, 54, 53, 52 and 51 frame.

Input arguments of SoundEffect() function

Since we are talking about SoundEffect() it's better explains its three input parameters...
First parameter is the index of sound effect, that follows same numeration you see in "Sound SFX Indices list" of [Reference] panel of NG_Center program.
Second argument is a pointer to StrPosizione (StrPosition) structure, with the position where locate the sound: x,y,z
A sound effect indeed, when it is "local", has a position in 3d world, from where if will be stronger, while from far distance the sound will be lower.
Since the structure has this prototype:

typedef struct StrPosizione {
          int OrgX;
          int OrgY;
          int OrgZ;
}PosizioneFields;

We can use a little trick to get easy set this parameter: we can use the serie of x,y,z coordinate of some moveable (like Lara) giving the pointer of CordX field, in this way the SoundEffect() function will read the three value for x,y,z directly from that moveable structure.
We used this trick, typing as second argument:

&GET.pLara->CordX

Where that "&" is unary operator to get the memory address of following (at its right) variable or structure field.
If you wish that the sound was "global", i.e. it was always listened at max volume ignoring its position, just supplying as second argument the value NULL, that means: null pointer. In this case the SoundEffect() will be played as global, like it was a cd track from audio folder.

The third argument should be "Flags".
Really I don't know yet so fine the values of these flags, anyway:
- The flag 0x02 means: perform this sound also when it located underwater.
- While the flag 0x04 allows to change some setting of the sound, changing it.

My idea is that the frequency of that sound will be changed dynamically and replaced with the other value you type as flags, but moving of two hecadecimal digit at right.
For instance if you type as flags: 0x200004, you'll enable the change of frequency (for the presence of 0x004 flag) with the value of 0x20004 divided by 256, that in hexadecimal means removing last two headecimal digits, so you'll set as new frequency the value 0x2000 (8192)
- The flag 0x20 I know it exists, but I've not yet discovered its meaning. It could affect looped sounds or multiple sounds, like those sound effects with two or more sub-sounds to be performed in sequence.

Trying in game new shooting skill

Once you have updated the sources and built the new plugin.dll, try what happens in game when the robot detects lara...


It works enough fine...
We have tried using as script settings this customize:

Customize=CUST_STAR_WARS_ROBOT, 88, SWR_HURTING+SWR_INJURING_ON_TOUCH, IGNORE, IGNORE

Now we try to replace the SWR_HURTING with the SWR_KILLING flag:

Customize=                    CUST_STAR_WARS_ROBOT, 88, SWR_KILLING+SWR_INJURING_ON_TOUCH, IGNORE, IGNORE

In this way we can test also how it works the killing mode. Theoratically the lightning should have red color and lara should be fired and dying, screaming...
Build the script and verify what happens in game


A weirdness is that, when lara is already dead, she goes on to moan when robot hit her newly.
While she doesn't scream when it has been just hit by lightning.
To fix above problems we should:
- Check if lara is dead, and if she is, avoiding to do other shootings
- To do screaming her it's necessary she was yet alive, so we should remove the LGTN_KILL_TARGET flag from lightning flags but lettin only the LGTN_FIRE_LARA flag.
In this way she will be killed in short time, but she will have the time to scream before dying.


void ShootLightning(int SourceItemIndex, bool TestKill)
{
          int IdColor;
          WORD Flags;
          int IdParam;

          if (GET.pLara->Health <= 0) {
                    // lara is already dead: quit shooting
                    return;
          }

          Flags=LGTN_PLAY_SOUND+LGTN_ADD_GLOVE_LIGHT;

          if (TestKill==true) {
                    // red color
                    IdColor= Service(enumSRV.CREATE_COLOR_RGB_COMMAND, true, 60,0,0);
                    // flags to kill lara and to do scream her
                    Flags |= LGTN_FIRE_LARA;
                    Flags |= LGTN_LARA_SCREAM;
          }else {
                    // don't kill but only inury lara
                    IdColor= Service(enumSRV.CREATE_COLOR_RGB_COMMAND, true, 0,0,128);
                    GET.pLara->Health -= 100;
                    GET.pLara->FlagsMain |= 0x10;
          }

Above are the changes to check if lara is already dead, and in this case the shooting function will quit.
Then we removed the LGTN_KILL_TARGET flag.


Get customizable also damage2 for Star Wars Robot

Now that we fixed last problems, we could get customizable also the damage for lara when the robot has SWR_HURTING flag.
We had already read from Enemy= command, for SW robot, the damage for touching the robot...
Since in Enemy command it is allowed set more damage types, we could to do the same also for damage on electric shock with SWR_HURTING flag.
Currently we had set the damage at 100 HP units:

else {
                    // don't kill but only inury lara
                    IdColor= Service(enumSRV.CREATE_COLOR_RGB_COMMAND, true, 0,0,128);
                    GET.pLara->Health -= 100;
                    GET.pLara->FlagsMain |= 0x10;
          }

We could consider it as default value, but when level builder use an Enemy= script command with robot slot, we should read if damag2 has been set, and in this case, we'll use that value inside of 100 hp as damage.
We can use almost same code we did in chapter: How to get settings from Enemy script command

Final release of ShootLightning() function

So this will be our final version of ShootingLightning() function:

// shoot a lightning from star wars robot to lara

void ShootLightning(int SourceItemIndex, bool TestKill)
{
          int IdColor;
          WORD Flags;
          int IdParam;
          short Damage2;

          if (GET.pLara->Health <= 0) {
                    // lara is already dead: quit shooting
                    return;
          }

          Flags=LGTN_PLAY_SOUND+LGTN_ADD_GLOVE_LIGHT;

          if (TestKill==true) {
                    // red color
                    IdColor= Service(enumSRV.CREATE_COLOR_RGB_COMMAND, true, 128,0,0);
                    // flags to kill lara and to do scream her
                    Flags |= LGTN_FIRE_LARA;
                    Flags |= LGTN_LARA_SCREAM;
          }else {
                    // don't kill but only inury lara
                    Damage2= 100; // default lost of HP for elenctric shock

                    // discover if there is a setting in Enemy commands for robot
                    if (Find(enumFIND.ENEMY_COMMAND, 499, -1, -1, -1, NULL)== true) {
                              if (FIND.pEnemy->TotDamage > 1) {
                                        if (FIND.pEnemy->VetDamage[1] != -1) {
                                                  // there is a valid value for damage2
                                                  Damage2 = FIND.pEnemy->VetDamage[1];
                                        }
                              }
                    }
                    IdColor= Service(enumSRV.CREATE_COLOR_RGB_COMMAND, true, 0,0,128);
                    GET.pLara->Health -= Damage2;
                    GET.pLara->FlagsMain |= 0x10;
          }
          IdParam=Service(enumSRV.CREATE_PARAM_COMMAND,
                                                  true,                    // Testdynamic = true
                                                  PARAM_LIGHTNING, // type of parameter: PARAM_LIGHTNING
                                                  Flags,                     // Flags LGTN
                                                  FromTomb4IndexToNgleIndex(SourceItemIndex), // Source item: index of star wars robot
                                                  FromTomb4IndexToNgleIndex(GET.LaraIndex), // Target item: index of lara
                                                  IdColor,           // Id of ColorRbg
                                                  32,                    // Intensity (default 32)
                                                  198,                    // sound effect (default 198 ELEC_ONE_SHOT)
                                                  16,                    // Size (default 16)
                                                  24,                    // Particle durate (default 24)
                                                  -1,                    // IntervalTime (default -1 continue)
                                                  1,                    // Alfa (default 1)
                                                  5,                    // Beta (default 5)
                                                  END_LIST);          // END_LIST (required for each CREATE_PARAM_COMMAND service)

          // call the trigger with a durate of 10 frames          
          PerformFlipeffect(NULL, 359, IdParam, 10);
}


Homworks

We can consider completed the Star Wars Robot, anyway there are yet some improvement we could imagine...


Changing the target point where lara will be hit.

Looking what happens in game, we see that the lightning will hit lara always on her head, or better, a bit over her head.
This happens because the Flipeffect 359, when you use as target point a moveable item index, will choose as target point the top Y coordinate of that moveable.
It should be interesting to be able to set as target point the same we use to verify if robot detected lara.
In RobotSW_DetectAndAttack() function, we perform some computations to discover what is the better Y coordinate to use with CheckDirection() function.
We set in YPosTarget variable the Y offset to change the Y origin of Lara:

          if (CheckDirection(GET.pItem, HeadHeight, GET.LaraIndex, YPosTarget, 0x2000, 0x2000)==true) {
                    // robot detects lara:

In some circustances, indeed, it maybe that this precise 3d point was not in Lara's head, but on her feet, or in the middle of her height.
We can save this position in this way:

          TargetX = GET.pLara->CordX;
          TargetY = GET.pLara->CordY + YPosTarget;
          TargetZ = GET.pLara->CordZ;

But how can we using the Flipeffect 359 with this new target position?
We can use a LARA_START_POS item that had our precise target position.
Then, when we are creating the Parameters=PARAM_LIGHTNING command, we can replace the input parameter for target position.
Where there was:

          FromTomb4IndexToNgleIndex(GET.LaraIndex), // Target item: index of lara

We can type:

          OcbValue + OTYPE_AI_DATA, // target item: LARA_START_POS with OcbValue

But how to have a LARA_START_POS item in target position we wish?
Well, we can create a new AI item in the position we wish and then use it as target argument...


The CreateAIObject() function
To create dynamically a new LARA_START_POS item, we can use the CreateAIObject() function:

int CreateAIObject(WORD Slot, DWORD CordX, int CordY, DWORD CordZ, WORD Room, int OcbValue, short Facing)

If you (as advisable) set for OcbValue argument the -1 value, this function will assign for you an ocb value to this new item, and it will choose an ocb value different by all others, to get it was univocal.
So we can create this LARA_START_POS item (before creating the Parameters=PARAM_LIGHTNING command) in this way:

          OcbValue= CreateAIObject(enumSLOT.LARA_START_POS, TargetX, TargetY, TargetZ, GET.pLara->Room, -1, 0);

Then we'll use (as already shown) our OcbValue to set the target point of the lightning:

          OcbValue + OTYPE_AI_DATA, // target item: LARA_START_POS with OcbValue

In this way, the target position of the lightning will be own that point where we detected the presence of Lara and not always and only the head.
Anyway there is yet an issue to manage...


How to delete an AI item that we created
We have to remember to delete the AI item we created, otherwise everytime the robot shot a lightning we'll have a new record AI until to waste all available AI records.
To delete an AI object we use this function:

void DeleteAIObject(WORD Slot, WORD OcbValue, bool TestAlls)

We'll give as input parameters: the slot of this new item (a LARA_START_POS in our case), the OcbValue we got from CreateAIObject() function when we created it, and we set "false" as TestAll because we have to delete only one item with that ocb value.
So we'll use this code to delete our LARA_START_POS item:

          DeleteAIObject(enumSLOT.LARA_START_POS, OcbValue, false);

But now we have a problem to solve: when should we delete this item?
Because we cann't deleting it immediately after the creation of Parameters=PARAM_LIGHTNING command, because for some frame (10 frame as we set in our code) the lightning will be performed and it will require for all that time the presence of our LARA_START_POS item.
Pratically we should remember to delete it but only when the lightning it has been completed and the LARA_START_POS item is no more used.
We could use a frame counter for this target: to count how many frames are elapsed from lightning to delete the LARA_START_POS item.
Since we have already a frame counter that it will be started own when we shot a lightning: the value we store in Reserved_3A field, we can use this variable also for this target.
Our frame counter will begin with 60 (frames) and then it will be decreased.
Since the lightning should elapse only 10 frames, we can set a safe value as 30 to delete our LARA_START_POS item, since it will be elapsed 30 frames and the lightning should be surely already completed for that moment.
So our code to remember to delete the LARA_START_POS could be like this:

          if (GET.pItem->Reserved_3A == 30) {
                    DeleteAIObject(enumSLOT.LARA_START_POS, OcbValue, false);
          }

We can place above code in any position of ControlRobotStarWars() procedure, where it will be always performed, so we could place this code immediatly after beginning rows of ControlRobotStarWars() function:

void ControlRobotStarWars(short ItemIndex)
{
          DWORD Flags;

          Get(enumGET.ITEM, ItemIndex,0);
          if (TriggerActive(GET.pItem)==false) return;

          if (GET.pItem->Reserved_3A == 30) {
                    DeleteAIObject(enumSLOT.LARA_START_POS, OcbValue, false);
          }



Where does it save the OcbValue?
There is yet another adjustment we have to do to our code...
We cann't save the Ocb value in a local variable, because we set that value in RobotSW_DetectAndAttack() function, but then we'll use it in ControlRobotStarWars() function.
A local variable keeps its value only inside of same function.
More, also if we typed our code to delete ai object in same RobotSW_DetectAndAttack() function where we set it, it will not work, because local variables lose their value just when the code of that function will be completed and in this case we have two different times: a moment when we set the value in OcbValue (when we created the item) and then, 30 frames later, when we use that value to delete the item.
To avoid these problems we have to use a global variable, that will preserve the value in any position and time.
We could create a new global variable in our StrMyData{} structure that we find in "structures_mine.h" source, adding a new row inside for this variable:

typedef struct StrMyData {
          StrSavegameData Save; // variable that it will be saved and restored to/from savegame
          // FOR_YOU:
          // define here all your global variables, i.e. those variables that will be seen from all procedures and keep
          // their value between all cycles of the game.
          // after you defined a variabile like:
          // int Alfa;
          // then you'll be able to access to them using syntax like:
          // MyData.Alfa = 3;
          // if (MyData.Alfa == 5) .
          int TotProgrActions;
          int LastProgrActionIndex; // used in case of overloading
          int FlareVSpeed;
          short OcbValue; // used to save ocb value of LARA_START_POS record we created as target for the lightning

Then we'll use in the code in this way:

          MyData.OcbValue

I.e., everytime we used "OcbValue" we'll have "MyData.OcbValue".
Anyway there is another idea we could have...
We finished the global variables of robot structure, about the "Reserved_XX" fields, but there is yet the "OcbCode" of this structure that we have not yet used in any way.
So we could use it for this reason: to store the ocb value of AI record we created.
In this case we'll use the "GET.pItem->OcbCode" to save the ocb of created item, and also to delete that item, later.

Shooting only when Robot is head on Lara




Looking above image we see a situation where robot detected lara and it shoots her.
It's normal that in above position the robot was able to see lara, and (for instance) to give the alarm when we are in SWR_SUPERVISORY mode, but it's a bit weird that the lightning moved diagonally while the robot is looking forward.
Perhaps it should be better if robot shot a lightning only when its head is head on that of Lara.
To force this new behaviour we should rewrite all code and animations, but there is an easy trick to get same result: if we, after robot detected lara, perform a second control to verify if robot is really looking head on lara...
In the case it was not doing, we'll skip the shooting of the lightning.
We should work on this code of RobotSW_DetectAndAttack() function:

          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]);
                    }else {
                              // it's missing SWR_SUPERVISORY flag

When lara it has been detected but it's missing the SWR_SUPERVISORY flag, we shot the lightning, but now we could do another control to verify if the facing (where is is looking) of the robot is enough right to have lara in front of it.
In the case it was not, we'll not shoot the lightning.
To perform this compute we'll have to compare two values:

Then we'll compute the difference between these two values using the AbsDiffO() function to see how much is wide the difference, and we'll shoot the lightning only it is lower that a given value, for instance 512.
In this way the robot will detect lara (as in the past) with a wide angle view, but it will shoot lara only the its head is looking own to head on lara.




Exercise 5: The Swinging Crane

This exercise begins in room 19 of plugins project


We had already seen this crane in one of last episodes of Tomb Raider games.
It was a trap, swinging on the ceiling, trying to move over lara and when it reached that position, the crane was left falling down to injury or killing lara.
Well, we'll re-build the code to have same behaviour in our plugin, anyway in this case we'll add also another feature for this crane: the chance that it was lara (the player) to drive it, to lift up obstacles and move them.
Anyway this topic will be treated in next exercise, in current chapter we'll work on the Crane like a running trap.




The problem: We finished the slots for new objects

Unfortunately, own building this tutorial, I discovered that Wad Merger program had the fixed limit of 500 slot for moveable items.
Since the last object, the ROBOT_STAR_WARS, had slot with id = 499, now we cann't add new slots otherwise wad merger will be not able to manage them and the wad should become very messed (I tried...)
So now we introduce the solution for this problem, since we mean build many other new objects in the future with our plugins.

The AssignSlot command to support new Objects

The AssignSlot= script command is not a news, it exists from many time, from when the first boats have been added to trng.
We can use own AssignSlot to allocate new objects in any existing tomb raider slot.
The method is the same we know:
With assignslot we inform trng that we placed in a common slot (for instance an animating slot) a new object with hardcoded management, (like kayak) and the engine will treat that common slot with right procedure to manage the special object.
We can do the same, using own OBJ_ mnemonic constants.
Since these OBJ_ constants will be private for each plugin, it's not important that they had a value different than other OBJ_ constants of trng or of other plugins.
They will be managed only by our plugin.

How to manage new OBJ_ mnemonic constants

Everytime we are writing the code to manage a new object, we should declare a new OBJ_ constant to describe that kind of object.
We'll declare it in "constants_mine.h" source:

// constants to assign slot for my new objects
#define OBJ_SWINGING_CRANE 1

But also in the .script file, since it will be the the level builder to type the AssignSlot command to inform us, where he put the new object.
So in the .script file of our plugin we could type a text like this:

OBJ_SWINGING_CRANE:1 ;Used with AssignSlot= command.
If you mean use the Swinging_Crane object supported by plugin_demo.dll, you have to copy the crane object in some slot (better an animating slot) and then type an AssignSlot command to inform in what slot you copied the crane.
For instance, if you copied crane to Animating5 slot, you should type this command:
AssignSlot= ANIMATING5, OBJ_SWINGING_CRANE

Notes:
- Above is the expanded text, but first to save it on disk, you'll have to remove NewLine character, replacing them with '>' character. You can use TrngPatcher or TextEditor to perform this operation
- The description of OBJ_ constant, it's a good side where type a description about the new object, to inform level builder about its usage, animations ect.


How to read from code the data of AssignSlot commands

Once you saved the .script file and restarted NG_Center to do reload also the .script file, you'll find in MNEMONIC CONSTANTS list also our new OBJ_SWINGING_CRANE constant.
When level builder will use it with a command like this:

AssignSlot=          ANIMATING2, OBJ_SWINGING_CRANE

Our plugin will receive this command, and in the code we can discover what slot contains the SWINGING_CRANE, in this way:

          int SlotCrane;

          // discover what slot id it has been assigned to host Swinging Crane object
          
          if (Find(enumFIND.ASSIGN_SLOT_MINE, OBJ_SWINGING_CRANE, -1, 0, 0, NULL)==false) {
                    // no assignslot for crane. Probably it's missing in current level
                    return;
          }

          SlotCrane= FIND.SlotAssigned;


With above code we get in SlotCrane variable the id of slot where there is the crane.
We'll use this value to get the slot structure where initialise right procedure for the crane.

Note: As general rule, it's better performing the Find() of assign slot only once, in InitSlot...() function (in our case it will be InitSlotCrane()), while in other side of the code, when you wish get the slot structure of our object, we'll use the SlotId field of the item structure, where the right value is already present:

          Get(enumGET.SLOT, GET.pItem->SlotID ,0);



Setting default procedures for Crane object

Crane is our third new object, so now we should know very well how to initialise a new object.
We have to prepare the three standard function in our source:


The Initialise() function

void InitialiseCrane(short ItemIndex)
{

}



The Collision() function

void CollisionCrane(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision)
{

}



The Control() function

void ControlCrane(short ItemIndex)
{
}



Initialise Slot structure for Crane items

Now to link the default function (Initialise(), Collision() and Control() functions) to our slot, we nee to initialise the slot.
So we create a function for this target and we'll type the code to call it from the cbInitObjects() function:

void InitSlotCrane(void)
{
}

And we add a call for above InitSlotCrane() from the code of cbInitObjects() function:

void cbInitObjects(void)
{
          InitSlotRobotCleaner();
          InitSlotRobotStarWars();
          InitSlotCrane();
}


Typing the basic code for our new Object

Now we created all main functions for the Crane but these function are yet empty.
We have to type the basic code for them.


Code for InitSlotCrane() function
Since this object is very alike than SW Robot and Robot Cleaner (it cann't be killed and it moved in the space), we can use almost same code, but setting the specific function for crane object, of course:

void InitSlotCrane(void)
{
          int SlotCrane;

          // discover what slot id it has been assigned to host Swinging Crane object
          
          if (Find(enumFIND.ASSIGN_SLOT_MINE, OBJ_SWINGING_CRANE, -1, 0, 0, NULL)==false) {
                    // no assignslot for crane. Probably it's missing in current level
                    return;
          }

          SlotCrane= FIND.SlotAssigned;

          // get slot structure with crane:
          Get(enumGET.SLOT, SlotCrane, 0);
          if ((GET.pSlot->Flags & enumFSLOT.PRESENT) == 0) {
                    // the flag PRESENT is missing, so there is NO Crane in this level: we quit immediatly the code
                    return;
          }

          GET.pSlot->pProcInitialise = InitialiseCrane;
          GET.pSlot->pProcCollision = CollisionCrane;
          GET.pSlot->pProcControl = ControlCrane;
          GET.pSlot->pProcDraw = (void *) 0x44f600; // default draw procedure


          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;
}

In above code we saw a news:

          GET.pSlot->pProcDraw = (void *) 0x44f600; // default draw procedure

In this case we set also a value for "pProcDraw" field of slot structure.

The default draw procedure

In other objects we have not set anything because new next generation slots, like ROBOT_CLEANER and ROBOT_STAR_WARS, had already set itself the default draw procedure.
But now, using an assignslot to host the swinging_crane, we cann't know where the level builder placed this object, and if he has been so mad to place it in some special slot like FIREROPE or TORCH_ANIM, it's not sure that the Draw procedure set in that slot was right for our crane.
So we set the address where in tomb raider there is the default draw procedure for animating and baddies moveables: it begins at 0x44f600 offset.
Everytime we initialise slot for moveable allocated with AssignSlot we should always remember to set a specific value for "pProcDraw" field, to avoid surprises.


Code for CollisionCrane() function
Also in this case the behaviour will be very alike than other robots: when lara will be touched by crane she will be injuried.
Anyway in this case we will not push away lara, because with the top-down movement it doesn't work fine.
We'll only hurt her when there is a collision:

void CollisionCrane(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 crane are in collision
          // reduce health of lara (at least if she was not already dead)
          if (pLara->Health > 0) {
                    // toggle HP value
                    pLara->Health -= 100;
                    // sound for LARA_INJURY
                    SoundEffect(31, &GET.pLara->CordX, 0);
                    // show HP bar on the screen
                    pLara->FlagsMain |= 0x10;
          }
}



Code for InitialiseCrane() function
Also for initialise we'll type a code alike than other new objects: we'll set first animation and state-id and we'll initialise global "reserved" variable that we should use in Control() code, anyway in this case we'll change something about flags since the crane will be always visible and the triggering will be used only to do move it.

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

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

          Get(enumGET.SLOT, GET.pItem->SlotID ,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;
          // force Y coordinate to move crane hanged on the ceiling of current room
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          GET.pItem->HeightFloor = FLOOR.FloorHeight;
          GET.pItem->CordY = FLOOR.CeilingHeight + 1200;
}

In above code we see a good example about a preparing operation to perform in Initialise() function.
Since the crane has to be hanged at the ceiling as default position, the code will move itself the crane in this ideal position, ignoring current Y position of the crane but using only X and Z position it has been set by level builder.
To discover ideal Y position, the code check the floor of sector where the crane it has been placed:

          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);

Then it uses the infos in FLOOR structure to initialise the crane item structure.
The FloorHeight will be coopied to HeightFloor field of crane structure:

          GET.pItem->HeightFloor = FLOOR.FloorHeight;

And then the height of the ceiling in that sector, it will be used to compute the Y position of the crane, to get it was hanged on the ceiling:

          GET.pItem->CordY = FLOOR.CeilingHeight + 1200;

We had to add 1200 to ceiling position, because the origin of the crane is placed in its lower side, but we wish getting that it was hanged on the ceiling by its higher side, so we added 1200 to compensate the crane height from its base.

How to plan the Control() code

Before typing code for ControlCrane() procedure it should be better to plan how the crane works.
We could type a list of rules followed by crane in automatic mode:
  1. The crane will move horizontally always attached to ceiling

  2. It will move verticaly, up/down, only when it stopped, horizontally, at center of given sector.

  3. It will be able to change its horizontal direction only when it is at middle of given sector

  4. It will be able to move horizontally only following hortogonal direction (and not diagonal)

  5. It will change its horizontal direction to try to get closer to Lara

  6. When it reached same sector (over it) where there is Lara, the crane will be left falling down to try to hit Lara

  7. The crane will be able to change horizontally its direction, only when the sector where it wish turn has same height of current ceiling (it moves only on "flat" ceiling)


Differences between other robots and the Crane

Once important difference between crane and robots (cleaner robot and SW robot) is that, with robots we changed their facing to do move them always respect to their current facing, while the crane will have always same facing and, in spite of this fact, it will move in all four directions, so simetimes it will be like it moved backwards respect its facing.
This means that when we wish move it at its left we'll use an absolute facing, ignoring its current facing.
For this reason we need to have an extra variable to store the direction facing, since it will be different that its current facing.
For instance we can use "Reserved_34 " field of its structure.

Another difference is that the Crane will be able to move also upstairs/downstairs, while this didn't happen with other robots.
So we should thinking about a vertical speed, when it will fall down and it will be pulled up newly.
There is already a "SpeedV" field in item structure for that target, but the problem is that field is hardcoded, this means that when you place a value there, the AnimateItem() function will move itself that item following that vertical speed, with a simulation of gravity attraction.
In some circustances it could be useful but we prefer keeping a full control on our vertical movements, also because, while with automatic mode, with the crane that will fall down, the gravity simultation should be fine, when we'll work with manual mode, the moving down/up should be controlled, more slow, and the gravity simultation should be not fine in that circustance.
So we'll use another reserved field to store "our" vertical speed, letting always to 0 the real "SpeedV" field to avoid interferences by AnimateItem() function.
We could use "Reserved_36" field to store current vertical speed.


Reserved variables used by Swinging Crane

So we have already assigned two reserved fields:

Reserved_34 : It will contain enumORIENT. values to set current horizontal direction
Reserved_36 : it will contain vertical speed. Negative values when it is moving down, and positive values when it is coming back up.

Now we should remember to initialise above variables and we did this operation in InitialiseCrane() as usuale:

          // force Y coordinate to move crane hanged on the ceiling of current room
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          GET.pItem->HeightFloor = FLOOR.FloorHeight;
          GET.pItem->CordY = FLOOR.CeilingHeight + 1200;

          // inizialize global variables
          GET.pItem->Reserved_34 = GET.pItem->OrientationH; // current horizontal direction
          GET.pItem->Reserved_36 = 0; // vertical speed

Note that we set as current horizontal direction the same facing that it has been set as crane facing. Anyway it's only a random value because our code in ControlCrane() function will try to detect the better hortogonal direction to reach lara.

Code for ControlCrane() function

As we disclosed, the crane will work in two different modes:
  1. Automatic mode.
    In this mode, the crane will work like in past tomb raider adventures. It will try to move over Lara and then it will fall down to hit her.

  2. Manual mode.
    In this mode, the crane will be droven by lara/player, and it will be able to pull up items.


We have to set immediatly how to discover if crane is working in one way or another.
So we can set that some flags in OCB field of crane item will affect this setting.
For instance: ocb 1 means "Automatic crane", while ocb 2 means "Manual Crane"
Since it's always better having mnemonic constants to remember the meaning of some values, we'll create these two mnemonic constants in "Constants_mine.h" source:

// Ocb flags for swinging crane
#define CRANE_OCB_AUTOMATIC 0x0001
#define CRANE_OCB_MANUAL 0x0002


Since these two modes are fully different we'll have to make two different control procedures, one for automatic and other for manual crane.
We'll use own the value in Ocb field to choose right procedure.
So we'll two new functions:

void ControlCraneManual(short ItemIndex)
{

}

void ControlCraneAutomatic(short ItemIndex)
{

}

While in the real ControlCrane() function, we'll call one or other of above function in according with values in Ocb field of Crane item:

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

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

          AnimateItem(GET.pItem);          

          // choose right control procedure
          if (GET.pItem->OcbCode & CRANE_OCB_AUTOMATIC) {
                    ControlCraneAutomatic(ItemIndex);
          }

          if (GET.pItem->OcbCode & CRANE_OCB_MANUAL) {
                    ControlCraneManual(ItemIndex);
          }
}

Now, in this first phase we'll concentrate all our code into ControlCraneAutomatic() while later, we'll make the code for manual crane.

Code for ControlCraneAutomatic() function

Main code of ControlCraneAutomatic() function should be this:

void ControlCraneAutomatic(short ItemIndex)
{

          int IncX;
          int IncZ;

          // check if we are in moving down/up phase
          if (GET.pItem->Reserved_36 != 0) {
                    // if vertical speed is different than 0, we are moving up/down
                    Crane_MoveUpDown(ItemIndex);
                    return;
          }

          // we are not moving up/down...
          // verifying if we are at middle of current sector
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          if (FLOOR.SectorCoords.Radius < 16) {
                    // crane is in middle point: now we can verify if it's possible changing direction or moving down the crane
                    // if lara is closed (ignoring Y coordinates) to crane: get falling down the crane
                    if (GetMaxDistance(&GET.pItem->CordX, &GET.pLara->CordX, true) < 650 &&
                              GET.pLara->CordY <= FLOOR.FloorHeight &&
                              GET.pLara->CordY >= GET.pItem->CordY) {
                              // crane is (about) over same sector of Lara: enter in move-down phase
                              // set positive speed to move down the crane
                              GET.pItem->Reserved_36 = 16;
                              return;
                    }

                    // crane is far from lara but at center of current sector: discover if it is better change direction
                    Crane_SetNewDirection(ItemIndex);

          }
          // move the crane horizontally
          GetIncrements(GET.pItem->Reserved_34, &IncX, &IncZ, 32);
          GET.pItem->CordX += IncX;
          GET.pItem->CordZ += IncZ;
          // update position in the case it changed room
          UpdateItemRoom(ItemIndex);
}

As first step, we verify if the crane was moving up/down because in that case there will be a different code to perform.
To verify this situation just checking if variable used for vertical speed (Reserved_36) is 0 or less:

          // check if we are in moving down/up phase
          if (GET.pItem->Reserved_36 != 0) {
                    // if vertical speed is different than 0, we are moving up/down
                    Crane_MoveUpDown(ItemIndex);
                    return;
          }

When Reserved_36 is different than zero, crane is moving up/down and so we call the Crane_MoveUpDown() to handle this situation.
After the call to Crane_MoveUpDown() we used a "return;" because we need to quit the code, since the remaing code of ControlCraneAutomatic() function will work only for horizontal movements.
When crane has vertical speed =0, it means that it is moving only horizontally, and in this case we have to verify if it's possible change direction (to follow lara) or to begin the move-down action to hit lara.
Above these two action may start only when the crane is at middle of current sector, so we discover this chance checking the floor and verifying the distance between the crane coordinates and the middle point of current sector.
The distance between the crane coordinates and the middle point it will be stored in "FLOOR.SectorCoords.Radius" variable of FLOOR structure.

          // we are not moving up/down...
          // verifying if we are at middle of current sector
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          if (FLOOR.SectorCoords.Radius < 16) {

When above condition it's true, this means that the crane is at center of the sector and we'll have to check if it is over Lara and enough closed to hit her.
To perform this check the condition seems a bit complicated:

                    // if lara is closed (ignoring Y coordinates) to crane: get falling down the crane
                    if (GetMaxDistance(&GET.pItem->CordX, &GET.pLara->CordX, true) < 650 &&
                              GET.pLara->CordY <= FLOOR.FloorHeight &&
                              GET.pLara->CordY >= GET.pItem->CordY) {
                              // crane is (about) over same sector of Lara: enter in move-down phase
                              // set positive speed to move down the crane
                              GET.pItem->Reserved_36 = 16;
                              return;
                    }

We check if the distance in horizontal way (using TestIgnoreY as true in GetMaxDistance() function) is so little (less than 650 game units) to have the crane over same sector of Lara or very closed than that.
But then we have to check also the Y position to be sure that lara was in the space below the crane and not in some casted room, above or below that of the crane.
So we added also a condition to verify that lara was over the floor of same room where is the crane:

GET.pLara->CordY <= FLOOR.FloorHeight

Since the coordinate move upper the items when they became littler, if CordY of Lara is less or even the Y coordinate of the floor (below the crane), it means that lara is over (or with same coordinate) of current floor.
Then we checked also that lara was below the crane, because she could be also in some casted room above that of the crane.
So we compared Y coordinate of Lara with that of crane:

GET.pLara->CordY >= GET.pItem->CordY)

If Lara Y is bigger (greater or even) than Crane Y, it means that she is below the crane in 3d world.
Only when all above condition are true in same moment, we'll move down the crane, or at least, we start this action: setting the vertical speed at +16, a positive value that will move the crane downstairs in 3d world.

                              GET.pItem->Reserved_36 = 16;

When we start the moving down then we'll quit the code with:


                              // set positive speed to move down the crane
                              GET.pItem->Reserved_36 = 16;
                              return;
                    }

In this way, we know that, when the condition to move down the crane is false, we are yet in moving horizontal phase.
So we have to check if it is possible change direction or continue on that current but verifying further obstacles (walls).

                    // crane is far from lara but at center of current sector: discover if it is better change direction
                    Crane_SetNewDirection(ItemIndex);
          }

The Crane_SetNewDirection() function will have only one target: change (or less) the direction of the crane. It could change to avoid some obstacle (wall) or to follow lara and to get closed to her the crane.
At end we'll have only to move really the crane in horizontal way, using the current direction (stored in Reserved_34 field) and updating the room of the crane:

          // move the crane horizontally
          GetIncrements(GET.pItem->Reserved_34, &IncX, &IncZ, 32);
          GET.pItem->CordX += IncX;
          GET.pItem->CordZ += IncZ;
          // update position in the case it changed room
          UpdateItemRoom(ItemIndex);
}


How to move the crane to get it near to Lara

As we said, this function has the target to discover a valid direction (not against walls or holes in the ceiling) and the best to reduce the distance between the crane and Lara.
Since the movement of the crane will be always following hortogonal directions (and never diagonally) we'll change its position or on X axis or Z axis.
We'll choose always a direction to get the crane more near to Lara.


We can study the problem with above image.
The big X should be the crane, that we suppose in two different sectors: A sector and B sector (in different times)
The big L is Lara.
The red points are the precise pivot of their position, and you can see some possible values of crane and lara.
A method to choose the direction could be this:

Above method is about right but there are problems...


Using above image, and supposing that the crane was in A sector, the coordinates to compare should be: Crane-X = 1536 and Lara-X = 2048.
Since Crane-X is less than Lara-X we choose as direction SOUTH.
In this way the crane will be moved at south, until to reach the B sector.
Now we perform newly the comparison and now Crane-X = 2560 and lara-X is (yet) = 2048.
Now the Crane-X is greater than Lara-X, so we choose as direction NORTH.
But if lara doesn't change her position, the crane will continuosly move South-North, without reducing distance with Lara.
Probably in above situation we could have a better result if we performed the check first on Z axis, but in another position it will born newly same problem.
The trick in this case it's to choose always, as axis where move the crane, that where the distance with Lara is bigger.
In above example, indeed, it's clear that the distance on Z axis is greater respect the distance on X axis, so we'll use a movement on Z axis.
Using this new method, the movement will no happen always on same axis, because once we moved crane on (for instance) Z axis, then the distance on that Z axis will be gotten shorter, and next time for this reason, it will be used a movement on X axis.


The code for Crane_SetNewDirection() function


// Discover best direction for crane to go near to lara between legal directions
// globals: in GET.pItem is already present the structure of Crane
// in GET.pLara is already present lara structure
void Crane_SetNewDirection(short ItemIndex)
{
          short Direction;
          int GoodCeilingY;

          int DistanceX;
          int DistanceZ;

          // discover the height of ceiling where is the crane.

          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          // we'll use this value as reference for right ceiling height of next sector
          GoodCeilingY = FLOOR.CeilingHeight;

          // discover distance on X axis
          DistanceX = GET.pItem->CordX - GET.pLara->CordX;
          // Discover distance on Z axis
          DistanceZ = GET.pItem->CordZ - GET.pLara->CordZ;

          // discover what distance is greater
          if (abs(DistanceX) > abs(DistanceZ)) {
                    // distance on X axis is greater
                    // set direction to move on x axis (if it is possible)
                    if (DistanceX < 0) {

                              // crane is at north of Lara: trying to move at south
                              Direction = enumORIENT.SOUTH;
                    
                              if (IsValidCeiling(Direction, GoodCeilingY)== true) {
                                        // found valid direction
                                        GET.pItem->Reserved_34 = Direction;
                                        return;
                              }
                    }else {
                              Direction = enumORIENT.NORTH;
                    
                              if (IsValidCeiling(Direction, GoodCeilingY)== true) {
                                        // found valid direction
                                        GET.pItem->Reserved_34 = Direction;
                                        return;
                              }
                    }
          }
          // now try to move on Z axis                    
          if (DistanceZ < 0) {
                    // crane is at west of Lara: trying to move at east
                    Direction = enumORIENT.EAST;
          
                    if (IsValidCeiling(Direction, GoodCeilingY)== true) {
                              // found valid direction
                              GET.pItem->Reserved_34 = Direction;
                              return;
                    }
          }
          if (DistanceZ > 0) {
                    // crane is at east of Lara: trying to move at west
                    Direction = enumORIENT.WEST;
          
                    if (IsValidCeiling(Direction, GoodCeilingY)== true) {
                              // found valid direction
                              GET.pItem->Reserved_34 = Direction;
                              return;
                    }
          }
          // if we reached this point of the code, it means that it's not possible moving to get close to lara.
          // so now we try to continue on old direction
          Direction = GET.pItem->Reserved_34;
          if (IsValidCeiling(Direction, GoodCeilingY)== true) {
                    // found valid direction
                    GET.pItem->Reserved_34 = Direction;
                    return;
          }
          // failed all attempts! Now we can only invert current direction and it will be surely valid since
          // it's from where crane came
          GET.pItem->Reserved_34 -= 16384; // 0x8000 (180 degrees)
}

At begin we read current height of celing and save this value because it will be used to verify if new sector, where we wish move, has same height.
This is easiest way to verify if it's possible moving on next sector.

          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          // we'll use this value as reference for right ceiling height of next sector
          GoodCeilingY = FLOOR.CeilingHeight;

Then we'll pass this value (GoodCeilingY) to a new function used to detect if it's possible moving in that sector.
This function works like IsFreeWay() function but it is more simple, since it's not foreseen that on the ceiling there was many obstacles.
This function, named IsValidCeiling(), will check only the height of ceiling in another sector in given direction respect current GET.pItem, has same height of GoodCeilingY value.


Code for IsValidCeiling() function


// verify if one sector in Direction from current GET.pItem structure, there is a ceiling with same height BaseCeiling
// and no other obstacles
// if is valid returns "true", otherwise "false"
bool IsValidCeiling(short Direction, int BaseCeiling)
{
          int IncX, IncZ;
          DWORD x, z;
          int y;

          GetIncrements(Direction, &IncX, &IncZ, 1024);

          // compute the coordinate of sector to check:
          x = GET.pItem->CordX + IncX;
          y = GET.pItem->CordY;
          z = GET.pItem->CordZ + IncZ;

          CheckFloor(x,y,z, GET.pItem->Room);

          // if there is a wall: no valid
          if (FLOOR.TestFullWall == true) return false;

          // if ceiling height is lower or higher: no valid
          if (FLOOR.CeilingHeight != BaseCeiling) return false;

          // if floor is over the Y coordinate of the crane: no valid
          if (FLOOR.FloorHeight < GET.pItem->CordY) return false;

          // ok this sector has a valid space to host the crane:
          return true;
}



Testing in game the horizontal movements of the Crane

Build the project, replace the pugin_.dll and try in game what happens when Lara enters in crane room.


It seems that it worked fine.
Crane follow lara but it avoids to move where there are columns, walls or the hole in the ceiling.
If you try for some moment the changes of direction you discover that it could happen that the crane stops and never more it moves to follow Lara.
It's normal, because that happens when crane is over the sector of Lara and in the code we set a vertical speed to do falling down the crane.
Anyway, since we have not yet made that code, the crane stops.
So, now we have to make the code to hit lara get falling down the crane when the vertical speed, stored in Reserved_36 field, is different than zero.
The code that will be performed in this situation is that of Crane_MoveUpDown() function.


Code for Crane_MoveUpDown() function


void Crane_MoveUpDown(short ItemIndex)
{
          int VSpeed;
          int MinBaseCeiling;

          VSpeed= GET.pItem->Reserved_36;

          if (VSpeed > 0) {
                    // vertical speed is positive
                    // the crane is falling down.
                    // we have to do gravity simulation: Increase the Vertical speed until it reaches a max value
                    VSpeed += 8;
                    if (VSpeed > 128) VSpeed=128;

                    GET.pItem->CordY += VSpeed;
                    
                    // we have to verify to have not reached or passed over the current floor height
                    if (GET.pItem->CordY > GET.pItem->HeightFloor) {
                              // forbid to enter inside of the floor
                              GET.pItem->CordY = GET.pItem->HeightFloor;
                              // since we reached the floor, now we have to do come back the crane
                              // setting a negative vertical speed
                              VSpeed = -32;
                    }
          }else {
                    // the speed is negative: the crane is coming back upward.
                    // we use a constant speed in this case and we have to check to have reached ceiling to stop
                    GET.pItem->CordY += VSpeed;
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    MinBaseCeiling = FLOOR.CeilingHeight + 1200;

                    if (GET.pItem->CordY < MinBaseCeiling) {
                              // reached the ceiling
                              GET.pItem->CordY = MinBaseCeiling;
                              // clear vertical speed
                              VSpeed=0;
                    }
          }
          GET.pItem->Reserved_36 = VSpeed;                    
}


Testing falling down of the Crane



It works enough fine. The crane falls down quicly, kills lara (when she is very closed) and then comes back up.
Anyway we have to complete the code about two issues:
  1. Changing the animations of the crane.

  2. Adding sound effects in according with its actions



The animations of Swinging Crane

Animation of the Crane

#Anim #Next StateId Description
0 0 0 Always Closed jaws
1 5 1 From closed to open
2 0 0 From open to closed
3 6 2 From open to large grabbing.
Note: the grabbing is when the jaws is a bit closer, like it was grabbing some object
4 5 1 From Large Grabbing to open
5 5 1 Always Open Jaws
6 6 2 Always Large Grabbing jaws
7 8 3 From large grabbing to slim grabbing
8 8 3 Always Slim Grabbing
9 5 1 From Slim Grabbing to Open
10 0 0 From Large Grabbing to Close
11 0 0 From Slim Grabbing to Close
12 8 3 From open to slim grabbing

For first exercise, of automatic crane, we'll use only first three animations:
We can open the jaw with animation 1, or close it with animation 2, or keeping close with animation 0.

We have to set how manage these animations in according with current action of crane.
For instance we could engage them in following way:


Change the code to engage animations

We have already set the animation 0 in InitialiseCrane() function.
So the starting phase with crane hanged on ceiling and close it's already made.
When we begin to move down the crane we have to set the animation 1.
So we add it, in ControlCraneAutomatic() function, to following code:

                    if (GetMaxDistance(&GET.pItem->CordX, &GET.pLara->CordX, true) < 650 &&
                              GET.pLara->CordY <= FLOOR.FloorHeight &&
                              GET.pLara->CordY >= GET.pItem->CordY) {
                              // crane is (about) over same sector of Lara: enter in move-down phase
                              // set positive speed to move down the crane
                              GET.pItem->Reserved_36 = 16;

                              ForceAnimationForItem(GET.pItem, 1, -1);
                              return;
                    }


Then we have to force animation 2 to close the jaw when crane reached the floor.
So we'll add it, in Crane_MoveUpDown() function, to following code:

                    // we have to verify to have not reached or passed over the current floor height
                    if (GET.pItem->CordY > GET.pItem->HeightFloor) {
                              // forbid to enter inside of the floor
                              GET.pItem->CordY = GET.pItem->HeightFloor;
                              // since we reached the floor, now we have to do come back the crane
                              // setting a negative vertical speed
                              VSpeed = -32;
                              ForceAnimationForItem(GET.pItem, 2, -1);
                    }


Crane bites Lara



Testing in game the new release it works fine: the crane opens its jaw when it is falling down and then closes it over Lara, biting her. Then the jaw will be closed and the crane comes back up.

How to add sound effects to our new Objects

We have already seen that to play a sound effect just calling the SoundEffect() function, passing to it the sound effect number, the pointer for three coordinates (x,y,z) and as third argument some flags. See Input arguments of SoundEffect() function
The issue in this situation is another...
What sound effect can we choose?
Because all sounds are already used by tomb raider objects. It's true that not all objects will be present in same level but when we type our code we cann't know what object will be present or less.
Another problem is that, once you chose a sound effect for some action of our object, other people (level builder) could be not agree with our choices.
If you are creating your plugin only for your own usage, then there is no problem. You choose the sound you wish, and if you, later, wish change it, just change the number in the SoundEffect() code.
Anyway, now we try to solve this problem when our plugin will be used by many other people and we need to give to them a chance to choose (customize) these sounds like they wish.
A method used in tomb next generation dll, is to use the script command: Customize=CUST_SFX, to set new sound effect for some prefixed sound types (TS_ values).
It works, and now we see how to use same method also with our plugin.

How to get the chance to customize the sounds for our new objects

To give (to the level builders) the chance to customize the sound effects of our objects, we'll use a method very alike that Customize=CUST_SFX script command but with some difference.
We cann't use that customize command, because it works for next generation engine and if all plugins use those values, there will be conflict, with two plugins using same numeric value (TS_) but for different targets.
So only one solution is that each plugin had its own Customize command that it will be reserved only for its sounds.

For this reason we cann't using the CUST_SFX constant, but we have to create a new own constant.

To avoid conflict with the names it's better inserting in this name also the name of our plugin.
For instance for this demo we can use own the "demo" word, so we'll create the mnemonic constant:

CUST_SFX_DEMO

But if your plugin is "plugin_mephisto" the constant will be "CUST_SFX_MEPHISTO" ect.

Another difference respect the CUST_SFT syntax, it's that command can having multiple instances in the script like:

Customize=CUST_SFX, TS_BINOCULAR_LIGHT, 45
Customize=CUST_SFX, TS_DETECTOR_SHOW, 63
Customize=CUST_SFX, TS_DIARY_NO_PAGE, 143

In spite this solution has some advantage it's a bit complicated to realize, so we'll use a more simple and plane method.
We'll have a single Customize=CUST_SFX_DEMO for level, and in this customize there will be many arguments, one for each sound type.
We'll type in Syntax of this customize the meaning of any sound, and when level builder don't wish change one of them, he can skip it using an IGNORE value (-1).


When we add new sounds, we'll change the Syntax: tag for our CUST_SFX_DEMO to add these new entries.

Adding sounds to Swinging Crane

As first step, we'll add some sound effect to the crane in according with the actions it is performing:

Theoratically we could use also same sound for two or more actions.
For instance the moving horizontally and the pulling up actions could having same sound.
While for the crane that touches the ground we could add also a short earthquake.
We'll use following sounds for above actions:

306 MAPPER_SWITCH_ON (to release down the crane)
309 MAPPER_MOVE (moving horizontally and pulling up)
314 HAMMER_TRAP_BANG (when touch ground)

While the 309 sound will be played continuosly (until the crane is moving), the other two will be played only in the precise moment when that action happens.
In the Crane_MoveUpDown() function, we play 309 sound when the crane is moving up:

                    if (GET.pItem->CordY < MinBaseCeiling) {
                              // reached the ceiling
                              GET.pItem->CordY = MinBaseCeiling;
                              // clear vertical speed
                              VSpeed=0;
                    }else {
                              // not yet reached ceiling
                              SoundEffect(309, &GET.pItem->CordX, 0);
                    }


And we play sound 314 when the crane reached the ground, and a little earthquake (always in Crane_MoveUpDown() function):

                    // we have to verify to have not reached or passed over the current floor height
                    if (GET.pItem->CordY > GET.pItem->HeightFloor) {
                              // forbid to enter inside of the floor
                              GET.pItem->CordY = GET.pItem->HeightFloor;
                              // since we reached the floor, now we have to do come back the crane
                              // setting a negative vertical speed
                              VSpeed = -32;
                              ForceAnimationForItem(GET.pItem, 2, -1);
                              SoundEffect(314, &GET.pItem->CordX, 0);
                              // earth quake: flipeffect 1
                              PerformFlipeffect(NULL, 1, 0, 0);
                    }

We'll play the 306 sound when we let falling down the crane, so we add soundeffect() in ControlCraneAutomatic() function:

                    // if lara is closed (ignoring Y coordinates) to crane: get falling down the crane
                    if (GetMaxDistance(&GET.pItem->CordX, &GET.pLara->CordX, true) < 650 &&
                              GET.pLara->CordY <= FLOOR.FloorHeight &&
                              GET.pLara->CordY >= GET.pItem->CordY) {
                              // crane is (about) over same sector of Lara: enter in move-down phase
                              // set positive speed to move down the crane
                              GET.pItem->Reserved_36 = 16;
                              ForceAnimationForItem(GET.pItem, 1, -1);
                              SoundEffect(306, &GET.pItem->CordX, 0);
                              return;
                    }

While at end of ControlCraneAutomatic() function we use yet the 309 sound when the crane is moving horizontally:

          // move the crane horizontally
          GetIncrements(GET.pItem->Reserved_34, &IncX, &IncZ, 16);
          GET.pItem->CordX += IncX;
          GET.pItem->CordZ += IncZ;
          // update position in the case it changed room
          UpdateItemRoom(ItemIndex);
          SoundEffect(309, &GET.pItem->CordX, 0);



The hardcoded sound effects in our code

Since we created also other objects, we can create arguments for sounds of cleaner robot and Star Wars Robot, other that the crane.
Looking for SoundEffect() functions in our code, we find following sounds to customize

Sound Effect used in our code

#Sound Name Function Description
183 THUNDER_CRACK CollisionRobotCleaner() It will be played when Cleaner Robot touch Lara killing her.
198 ELEC_ONE_SHOT ShootLightning() It will be used to create the Parameters=PARAM_LIGHTNING command to shoot the lightning to hit Lara.
We pass as argument the number of sound effect to use for this trng action.
306 MAPPER_SWITCH_ON ControlCraneAutomatic() It will be played when the crane will be released to fall down
309 MAPPER_MOVE Crane_MoveUpDown()
ControlCraneAutomatic()
It will be played when the crane is moving horizontally or when it is pulling up the crane
314 HAMMER_TRAP_BANG Crane_MoveUpDown() It will be played when the crane touch the floor after the falling down




The customize command for our sound effects

Now that we see the sound effect to customize, we can type in .script file the customize command with all right input parameters:

CUST_SFX_DEMO:2 ;Used with customize command
Syntax: Customize=CUST_SFX_DEMO, CleanerTouchLaraSfx, SWRobotShootsLaraSfx, CraneReleaseJawsSfx, CraneMovingSfx, CraneTouchFloorSfx

You can change the sound effect for the given sound type.
If you wish change some sounds and let unchanged the ohters, just you type an IGNORE in field where you wish left default sound.

Above is the text not yet compressed (replacing newlines with ">" characters).

Note: we assigned "2" as value for CUST_SFX_DEMO because there was already a CUST_ constant with value "1", the CUST_STAR_WARS_ROBOT:1.

As you can see, we described each input argument with the name of sound type to change.
It's important remembering the sorting we used, because then we'll have, from the code, reading the values of this customize, understanding their assignment.
To handle this situation it's better create some mnemonic constant (only for code usage) to remember the position of each sound.
Reading above syntax, the indices will have following numeric sorting:

#define SFX_CLEANER_TOUCH_LARA 0
#define SFX_SWROBOT_SHOOTS_LARA 1
#define SFX_CRANE_RELEASE_JAWS 2
#define SFX_CRANE_MOVING 3
#define SFX_CRANE_TOUCH_FLOOR 4

Above are the constant definitions we typed in "Constants_mine.h"
In same source we'll add also the new CUST_ constant, of course:

#define CUST_SFX_DEMO 2


How to read data from Customize=CUST_SFX_DEMO command

Now to discover the value set (for instance) for SFX_CRANE_RELEASE_JAWS sound, we'll use a code like this:

          if (Get(enumGET.MY_CUSTOMIZE_COMMAND, CUST_SFX_DEMO, -1) == true) {
                    // it exists a Customize=CUST_SFX_DEMO command in current level secton
                    SfxNumber = GET.pCust->pVetArg[SFX_CRANE_RELEASE_JAWS];
                    if (SfxNumber == -1) {
                              // it has not been set a change for SFX_CRANE_RELEASE_JAWS sound
                    }else {
                              // for SFX_CRANE_RELEASE_JAWS sound we'll use SfxNumber value
                    }
          }

We get the customize=CUST_SFX_DEMO, with Get() function.
If it exists (Get() returns "true"), we use the SFX_ constant to read the right value of customize array, then we verify if it is -1 (IGNORE) or less.
Since we should perform above code everytime we need to play a sound, it's better typing this code in a little function with a short name, to get more easy this operation.
We can name this function: GetSFX() and it will receive two input parameters: the SFX_ constant to locate the sound type, and then the default sound we used for that sound type, in this way the function will be able to return the new sound, or the default sound (when it's missing that customize) itself.

The GetSFX() function to read our customized sound effeects



int GetSFX(int SFX_Type, int DefaultSound)
{
          int SfxNumber;

          if (Get(enumGET.MY_CUSTOMIZE_COMMAND, CUST_SFX_DEMO, -1) == true) {
                    SfxNumber = GET.pCust->pVetArg[SFX_Type];
                    if (SfxNumber != -1) {
                              // it has been set a new sound for this sound type
                              return SfxNumber;
                    }
          }
          // no customize is present for this sound type: using the default sound
          return DefaultSound;
}

Then we use above function in this way: when we had a code like this (used when Cleaner Robot touches Lara):

                    // play sound 183 THUNDER_CRACK on coordinate of robot
                    SoundEffect(183, &GET.pItem->CordX, 0);

We'll replace it with new code:

                    // play sound 183 THUNDER_CRACK on coordinate of robot
                    SfxSound = GetSFX(SFX_CLEANER_TOUCH_LARA, 183);
                    SoundEffect(SfxSound, &GET.pItem->CordX, 0);

In above example we should also define a local variable like SfxSound, in each function where we perform this replacing.
Anyway we could type above code also in this way:

                    // play sound 183 THUNDER_CRACK on coordinate of robot
                    SoundEffect(GetSFX(SFX_CLEANER_TOUCH_LARA, 183), &GET.pItem->CordX, 0);

Where we used directly the value returned by GetSFX() function, to pass that value as first argument of SoundEffect() function, saving the usage of a new temporary local variable (SfxSound).
Both form are valid:the first is more understandable but it requires a new local variable, while the second is a bit messed but it saves a variable and it's a bit faster.
Now we have to locate all SoundEffect() function where we are playing some of customizable sounds, and replace the SoundEffect() call like we have just seen.
You can use the Find in Files function to have a list of all source lines where we used a SoundEffect.


Type "SoundEffect" instead by "cbCycleBegin" and click on [Find All].
Then, on the list you can perform a double click on that found line to move the source in that position.
Ignore the SoundEffect() with sound not customized, like the 31 (LARA_INJURY) and change all others like we saw.
Everytime you find a soundeffect like this:

SoundEffect(NumberOfSfx, &GET.pItem->CordX, 0);

You change it in this way:

                    SfxSound = GetSFX(SFX_TypeSound, NumberOfSfx);
                    SoundEffect(SfxSound, &GET.pItem->CordX, 0);

Where in above example the "SFX_TypeSound" will be one of SFX_... constant to describe the sound type:

#define SFX_CLEANER_TOUCH_LARA 0
#define SFX_SWROBOT_SHOOTS_LARA 1
#define SFX_CRANE_RELEASE_JAWS 2
#define SFX_CRANE_MOVING 3
#define SFX_CRANE_TOUCH_FLOOR 4

Once you replaced all instances, build the project and try in game how it works.
Since we have not yet added a Customize=CUST_SFX_DEMO command in the script, the sounds should work like in the past.
Then we can perform another experiment: adding a Customize=CUST_SFX_DEMO command in the script, changing some sound and letting unchanged others (with IGNORE) and then verify in game if all required sounds have been modified as we have set.

What is there upstairs?

Now we try to explore the room with the crane.
We'll see there is a picup (a trident) and a door.


When we pick up the trident the door will open.
Now we try to go upstairs in the room placed over the ceiling of crane room.
Once we across the door, we'll find a long wall to climb.
When we are in upstairs room we see a bad thing...


That gray pole is a part of the crane of downstairs room.
When lara is in downstairs room we'll see that pole only when the crane is moving down, while when it is hanged on the ceiling it's not visible but only because the pole is sinked in the ceiling.
When there is an upstairs room (over that of the crane) we have the problem of above picture: the normally hidden pole, it will be very visible in upstairs room.
We could let to level builder the solution: for instance, avoiding to build a room above that with the crane, anyway we can try to give an help to solve this problem with a little trick...

Since the pole it will used, and it should be seen, alway when lara is below the crane, better in same room of the crane, we could set as invisible the mesh of the pole when lara is over the crane room, while to get it newly visible when lara is below the crane.
To do this change we need to place our code where it will be always performed.
So we'll type this code at begin of ControlCrane() function:

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

          Get(enumGET.LARA,0,0);

          if (GET.pLara->CordY < GET.pItem->CordY &&
                    GET.pItem->Reserved_36 == 0) {
                    // lara is above the crane and the crane is hanged on the ceiling: set invisible the first (pole) mesh
                    GET.pItem->MeshVisibilityMask &= ~1;
          }else {
                    // conditions are not true: get visible the first (pole) mesh
                    GET.pItem->MeshVisibilityMask |= 1;
          }

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

We perform the computation first of:

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

Because also when the crane is not moving it is anway visible and it could create that problem of pole in upstairs rooms.


We check if lara Y is less than crane Y, because this means that lara is over the crane in 3d world but we check also that the vertical speed (stored in Reserved_36 field) was 0, because otherwise it maybe happen this situation:
Lara is in the crane room.
The crane moves down to hit lara.
Lara jump to avoid the crane.
And in this moment Lara is over the crane... so, missing the condition about vertical speed, the pole will be removed but in that situation it should be visible.
Using also the condition about vertical speed we'll get invisible the pole, only when the crane is hanged on the ceiling to avoid this problem.

Testing the disappearing of crane Pole



Trying in game the new code, we can verify what happens.
We see that, when lara is in crane room, the pole is visible (left side of above picture), while going upstairs, we see that the pole is no more visible now.

To put a shadow or less?

About that shadow below the objects, we used it for Star Wars Robot but we didn't for Cleaner Robot since it had no space between its body and the floor.
About the crane it's not so easy to decide, because the crane is very far from the floor and so the shadow may be exist, but also so far it's not sure it was realistic.
By other hand, for player should be an help to be able to see the shadow on the floor, since it's not so easy to look upstairs to see where is in that moment the crane.
In the doubt, we'll let choosing to level builder, using an OCB flag for this target.
So we create a new mnemonic constant in "constants_mine.h" source:


// Ocb flags for swinging crane
#define CRANE_OCB_AUTOMATIC 0x0001
#define CRANE_OCB_MANUAL 0x0002
#define CRANE_OCB_SHADOW 0x0004

Then we'll check if it is present this setting in ocb field of the crane, and we'll enable or less the shadow.
We can place our code at begin of ControlCrane() function:

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

          Get(enumGET.LARA,0,0);

          if (GET.pLara->CordY < GET.pItem->CordY &&
                    GET.pItem->Reserved_36 == 0) {
                    // lara is above the crane and the crane is hanged on the ceiling: set invisible the first (pole) mesh
                    GET.pItem->MeshVisibilityMask &= ~1;
          }else {
                    // conditions are not true: get visible the first (pole) mesh
                    GET.pItem->MeshVisibilityMask |= 1;
          }

          // get the slot of crane
          Get(enumGET.SLOT, GET.pItem->SlotID, 0);

          if (GET.pItem->OcbCode & CRANE_OCB_SHADOW) {
                    // show the shadow
                    GET.pSlot->FootStep = 256;
          }else {
                    // remove the shadow
                    GET.pSlot->FootStep = 0;
          }

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

Build the source and update the .dll in trle folder, but now we have also to add the ocb 4 in ocb of crane in level map.
Since there is also the ocb 1 (to set automatic crane) we'll have to type 5 as new ocb value.
Then build the level and try in game how the shadow work...

A weird shadow



Ok, it works, but looking above picture we see a weird thing...
It's odd that the shadow at left, when the crane is hanged on the ceiling (and very far from floor) has same size of the shadow at right, when the crane is touching the floor.
Probably this is standard behaviour of shadows in tomb raider but with the crane, that move also vertically from wide heigh, it seems too bad: unacceptable.

How to create a proportional Shadow

So, we try to change the size of the shadow in according with the distance between the crane and the floor.
When the crane is very far the shadow will be very little, while when the crane is on the floor (no distance) the shadow will have max size.
Since to make this computation we need of some rows of code, it's better creating a little function for this target, and then we'll call it from ControlCrane() code.

// globals already set: GET.pSlot, GET.pItem
void Crane_ShowShadow(void)
{
          int BaseCeilingY;

          int DistanceNow;
          int DistanceMax;
          int CurrentSize;

          // show the shadow
          // compute the size in according with distance between floor and the crane
          // max value of 256 when the crane is touching the floor
          // min value 64 when the crane is hanged on the ceiling.
          
          // first we compute the Y position where it should be the crane to be hanged on the ceiling (max farness)
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          BaseCeilingY= FLOOR.CeilingHeight + 1200;
          // now we find the max possible distance: between the floor and BaseCeilingY
          DistanceMax = FLOOR.FloorHeight - BaseCeilingY;
          // now we find the real current distance between floor and crane
          DistanceNow = FLOOR.FloorHeight - GET.pItem->CordY;

          // now we apply the ration of shadow size with this computation:
          // when the distance is max, the size will be 64, while when the distance is 0 the size will be 256

          CurrentSize = 192 - (DistanceNow * 192 / DistanceMax);

          CurrentSize += 64;

          GET.pSlot->FootStep = CurrentSize;
}

We named it "Crane_ShowShadow" and the computation works on the distance between crane and floor (DistanceNow) and the max distance (ceiling position and floor)
In this way we'll have different size of shadow in according with the current distance.
Now in code of ControlCrane() function we'll call our Crane_ShowShadow() function:

          // get the slot of crane
          Get(enumGET.SLOT, GET.pItem->SlotID, 0);

          if (GET.pItem->OcbCode & CRANE_OCB_SHADOW) {
                    Crane_ShowShadow();
          }else {
                    // remove the shadow
                    GET.pSlot->FootStep = 0;
          }

Now we try in game how it works...

The new proportional shadow of the Crane



It works enough fine...
When the crane is at large distance (hanged on the ceiling) the shadow is very little, while when the crane is falling down, the shadow increases its size, until to reach max size when the crane touch the floor.


Setting for proportional shadow

In spite I think the proportional shadow worked better than fixed-size shadow, some guy could doesn't agree, so we can add an OCB setting to do proportional shadow, so who doesn't wish it, just he omits that setting.
So we'll add new mnemonic constant for OCBs:

// Ocb flags for swinging crane
#define CRANE_OCB_AUTOMATIC 0x0001
#define CRANE_OCB_MANUAL 0x0002
#define CRANE_OCB_SHADOW 0x0004
#define CRANE_OCB_PROP_SHADOW 0x0008

And now in Crane_ShowShadow() function, we'll skip the computation for poportional shadow if the CRANE_OCB_PROP_SHADOW ocb is missing:

// globals already set: GET.pSlot, GET.pItem
void Crane_ShowShadow(void)
{
          int BaseCeilingY;
          int DistanceNow;
          int DistanceMax;
          int CurrentSize;

          // show the shadow
          if ((GET.pItem->OcbCode & CRANE_OCB_PROP_SHADOW) == 0) {
                    // it's missing the Ocb for proportional shadow: using fixed size shadow and quit the function
                    GET.pSlot->FootStep = 256;
                    return;
          }
          // compute the size in according with distance between floor and the crane

Now we can try: adding or removing the value 8 (CRANE_OCB_PROP_SHADOW) from OCB field of the crane in level map, to see how it works.

Homeworks

We completed the automatic crane.
It was an easy object. Anyway we can project yet some improvement.


Adding a sound for change of direction
The sound when crane is moving it's a bit boring and it doens't give many information about what's doing the crane.
A method to improve this situation is to play another sound effect only when crane change its direction.
This change is almost always a turning of 90 degrees respect previous direction, anyway it's important understanding how to detect the right moment to play this sound.
In the ControlCraneAutomatic() function, we have this code to change the direction:

                    // crane is far from lara but at center of current sector: discover if it is better change direction
                    Crane_SetNewDirection(ItemIndex);

To discover when the direction has been changed, the easiest way is to save the old direction (from Reserved_34 field) before calling the Crane_SetNewDirection() function, and then check if the old direction was different than current direction.
So a code like this:

                    // crane is far from lara but at center of current sector: discover if it is better change direction
                    OldDirection = GET.pItem->Reserved_34;
                    Crane_SetNewDirection(ItemIndex);
                    if (OldDirection != GET.pItem->Reserved_34) {
                              // direction has been changed: doing a sound
                    }

We'll have to allocate the OldDirection local variable, but also to manage the new sound, getting customizable like others.
This means that we'll add a new SFX_ constant, and we'll change in the .script file the Syntax: prototype for Customize=CUST_SFX_DEMO command.
When we'll play the sound we'll use the new method reading the value with GetSFX() function like we have already seen.


Biting really Lara and taking her away

This homework is a bit complicated, in particular way, if you wish getting the perfection.
Anyway there are different levels you could try to reach.
The idea is to simulate the bite to Lara with her pulled up with the crane just a moment before going to title level.
I'll give you some suggestions if you wish try this final dramatization.


Where
The code where you should begin it's that in Crane_MoveUpDown() function, here:


                    // we have to verify to have not reached or passed over the current floor height
                    if (GET.pItem->CordY > GET.pItem->HeightFloor) {
                              // forbid to enter inside of the floor
                              GET.pItem->CordY = GET.pItem->HeightFloor;
                              // since we reached the floor, now we have to do come back the crane
                              // setting a negative vertical speed
                              VSpeed = -32;
                              ForceAnimationForItem(GET.pItem, 2, -1);
                              SoundEffect(GetSFX(SFX_CRANE_TOUCH_FLOOR, 314), &GET.pItem->CordX, 0);
                              // earth quake: flipeffect 1
                              PerformFlipeffect(NULL, 1, 0, 0);
                    }



When
This pulling up, should happen only when there are following conditions:
  1. Lara is dead. Her HP is 0 or less

  2. Current animation of Lara is 138 and about in first 50 frames, because later, lara is laying down, and it should be not realistic to be picked up in that moment

  3. Lara is enough near at middle of same sector where is the crane. Because, if the crane hit her only by side, lara is not in right position to be picked up.



How
Since default dead animation for HP loosing (138) ends with lara is laying down, you should prepare a new custom animation with realistic position of lara's body while she is picked up by the crane.
With this animation you should set a state-id like 89 (STATE_CONTROLLED) to avoid that engines affect position of Lara while you change its coordinates.
Once you set new animation for lara, then you can use own this animation nunber to recognize that lara is going to be picked up by the crane, and in Crane_MoveUpDown() function, everytime you add to crane the (negative) vertical speed to move it upward, you should apply same vertical speed to Lara coordinate Y, to move her togheter with the crane.



The End
When the crane reached ceiling, you should force the step to go to title, or to stop (from ControlCraneAutomatic() function, the movement of the crane, otherwise you should hande also next falling down, since the passage to title requires some second if no key has been pressed.
To force reloading title level after the dead of lara, in right moment, you can use the flipeffect:

; Set Trigger Type - FLIPEFFECT 53
; Exporting: TRIGGER(9:0) for FLIPEFFECT(53) {Tomb_NextGeneration}
; <#> : Keyboard. Simulate receivement of <&>keyboard comand in (E) way

; <&> : Action (and Enter)
; (E) : Single sending

Using the function:

PerformFlipeffect(NULL, 53, 9, 0);





Exercise 6: Driving the Crane

This exercise begins in room 25 of plugins project

We have already created a code for Swinging Crane but now we'll use same object for another target: that it was lara (the player), to drive it, to lift up obstacles and move them.
This new skill we'll be useful to introduce you about redirection of input commands, since when lara is driving the crane, it will be like it was a vechicle that she is driving and the input command like "Left", "right", "jump" or "action" will be used in game with another target respect the lara movements.
In spite that the crane it's not a vehicle, it will work following same procedure: the game changes, and it follows new rules, like when lara is driving the jeep, where, the leading actor is no more Lara but only a car that the player will have to drive.
Understanding very fine this logic it will be very useful to add many new features, really new, in our custom levels: new vehicles and machinery of any kind.
In this exercise we'll try to give the chance to Lara (and the player) to drive the crane to pull up and move many big items but only moveables.
The level builder should be prudent when he is planning the crane room, because the crane will move any item in any position, following the commands of driver, but there are circustances where some change of position for some item could mess the game.
For instance it's not allowed that a pushable object, with ocb to have walkable collisions, it was placed over another item (moveable or statics) but only over the floor.
Our code will give full freedom to move all items, regardless if final result will be problematic, so in the building of the room and the choice of items to place in this room, it will be important avoiding messed situations.




The Control Panel of the Crane

For this exercise we will use the same Swinging Crane of Exercise 5 (but with a different OCB value), anyway in this case we need also of another object: the control panel for the crane.
In the reality this object has no special animation but it is like a switch that we have to use to discover when lara is going to engage the drivable crane.
Since this object (a common animating with no animtions) has an hardcoded target, we require to know about what is the slot that hosts the control panel.
For this reason in this case we need of two AssignSlot= script commands: one for the slot of Swinging Crane, and another for the slot of ControlPanel.
So, we have to declare another OBJ_ costant: in the "constants_mine.h" source and also in our .script file:

// constants to assign slot for my new objects
#define OBJ_SWINGING_CRANE 1
#define OBJ_CONTROL_PANEL_CRANE 2

While in the .script file we add also a little description about the meaning of crane control panel:

OBJ_CONTROL_PANEL_CRANE:2 ;Used with AssignSlot= command
When you use the Crane with ocb 2 to enable the driving of the crane, you have to type an AssignSlot= command with OBJ_CONTROL_PANEL_CRANE constant, to inform what is the slot where the control panel of the crane it has been copied to.

The control panel is a simple animating from where lara will engage the driving crane. You can think to it, like a switch object, where just lara moved in front of it, and press ACTION (ctrl) to enter in "driving crane" phase.
If you have in ANIMATING3 slot the control panel of the crane, you should type a command like this:

AssignSlot=ANIMATING3, OBJ_CONTROL_PANEL_CRANE

Note: in the item you used as control panel, you should type as OCB the index of the Swinging Crane item that it will be engaged.
This is necessary to discover what swinging crane to do engage.


Note: as usual, I showed here the uncompressed version because it's more readable, but then you have to remove the new-line characters replacing them with '>' character.

So in the script we'll have two AssignSlot commands.
For our demo the commands will be:

AssignSlot=          ANIMATING2, OBJ_SWINGING_CRANE
AssignSlot=          ANIMATING3, OBJ_CONTROL_PANEL_CRANE

Since the crane is in ANIMATING2 slot, while the control panel is in ANIMATING3 slot.

How to initialise the control panel object

In spite the control panel has not a so active function in driving crane, we need to replace its default Collision() with ours, because we need to manage the situation when lara is closed to the control panel to verify is she has right position and game command (ACTION) to enter in "driving crane" mode.
So, in the cbInitObjects() function, we'll add a call for the initialization of the control panel:

void cbInitObjects(void)
{
          InitSlotRobotCleaner();
          InitSlotRobotStarWars();
          InitSlotCrane();
          InitSlotCtrlPanelCrane();
}

And then we add (above the cbInitObjects() function) the InitSlotCtrlPanelCrane():

void InitSlotCtrlPanelCrane(void)
{
          int SlotCtrlCrane;

          // discover if there is an AssignSlot for control panel of crane
          if (Find(enumFIND.ASSIGN_SLOT_MINE, OBJ_CONTROL_PANEL_CRANE,-1,0,0, NULL) == false) {
                    // it's missing. quit the code
                    return;
          }
          SlotCtrlCrane=FIND.SlotAssigned;

          // get the slot structure used by Control Panel:
          Get(enumGET.SLOT, SlotCtrlCrane,0);


          // verify that the slot was really present
          if ((GET.pSlot->Flags & enumFSLOT.PRESENT) == 0) {
                    // the slot is empty: quit the code
                    return;
          }
          // now we set our CollisionProcedure
          GET.pSlot->pProcCollision = &CollisionCtrlPanelCrane;
          // initialise all other procedures with default values for animating items:
          GET.pSlot->pProcInitialise=NULL;
          GET.pSlot->pProcControl = (void *) 0x4554f0;
          GET.pSlot->pProcFloor = NULL;
          GET.pSlot->pProcCeiling = NULL;
          GET.pSlot->pProcDraw = (void *) 0x44f600;
          GET.pSlot->pProcDrawExtras = NULL;
}

Note: the only our custom procedure whom we are interested is the CollisionCtrlPanelCrane() function, that we will make to handle the activation of the crane when lara enages the control panel.
Anyway we had set also all other procedures with default values, because we cann't know what kind of slot the level builder chose with AssignSlot command, and in the case it was a very particular slot, some other native procedures could affect in the game with some collateral effect.

The default procedures used by Animating objects

Everytime we mean giving some hardcoded feature to some slot assigned with AssignSlot command, we should rembember to reset all procedures not used by us, with default values for Animating objects, because the animating are the most traditional moveables.
In this way, we'll trasnform that slot in an animating slot, where we changed only some procedure, those we'll redirect in our code to reach our targets.
The default values to set are or NULL, that it means that it's not foreseen any procedure, or some hexadecimal value that should be the address in tomb raider program where begins the default procedure for animating objects.
This is the list of default procedures for animating objects:

pProcInitialise=NULL
pProcControl=0x4554f0
pProcFloor=NULL
pProcCeiling=NULL
pProcDraw=0x44f600
pProcCollision=0x446d60
pProcDrawExtras=NULL

When you wish set one of these (not NULL) values, you have to type a "(void *)" text before the hexadecimal value, otherwise you'll get an error message by compiler.
It happens because all these fields are pointers while a constant number (like 0x4554f0) is not a pointer. When you type the cast "(void *)", you say to the compiler: that you'll find at right it is a pointer, trust me!
It's for this reason that we typed a code like:


          GET.pSlot->pProcControl = (void *) 0x4554f0;


The collision procedure for Control Panel of the Crane

The declaration of collision procedure has always same arguments:

void CollisionCtrlPanelCrane(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision)
{

}

We'll place the common code to push away lara when she is entering in the object:

          Get(enumGET.ITEM, ItemIndex, 0);

          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 is colliding with the object: pushaway lara to move outside of collision box of the object:
          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);

Above is default code for (almost) all object with collision box, anyway there is a difference in this case.
We omitted the condition:

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

Because the control panel is always active, since it has the status of a common animating, so the collision it will work always.

Detect right position of Lara

If all code to type in Collision() procedure was that we have just seen, it should be futile using our CollisionCtrlPanelCrane() function, because we could let the default collision procedure that performed, exactly, same computations.
But we need to add other code to verify when lara is in right position respect the control panel and the player is hitting the ACTION game command.
This code is typical for all vehicles but also for many other object with those lara is able to interact.
We know the TestPosition script command. It keeps data to verify if lara is in right position respect some moveable.
Well we'll use same data but with no need to type really a TestPosition script command.
We'll use in direct way that data, in same way performed by tomb raider engine.
The matter is a bit complicated but very interesting to get that lara was able to interact with some item...

How to discover the TestPosition data for right alignment with the control panel

The easiest way to discover the right values for TestPosition command is to add to [Options] section of the script,the following commands:

Diagnostic=ENABLED
DiagnosticType=DGX_ERRORS, IGNORE
LogItem= FLI_SHOW_DIFFERENCES, 130

We enabled the diagnostic and the we added also a LogItem= command to have data about a moveable with index=130.
Since the control panel of the crane in the level map has the index 130, we'll see data about it.
The FLI_SHOW_DIFFERENCES flag is to enable to drawing of test position values between lara and the item.
So we'll play the game, will move lara in front of the item in the better position for alignment with the item, and then we'll copy the data we see on the screen:




Now we'll copy the data in a global StrTestPositionCmd structure.
To define a global variable just type its declaration outside of all function of "plugin_trng.cpp" source.
Usually we'll type above all functions and below the #include directive.

StrTestPositionCmd CtrlCraneTestPosition;

Since this variable is global, we can use it from all functions.
Our first step is to initialise its values, using the data of above image.
We can perform this code in InitializeAll() function, since we need to perform this operation only once for whole game and InitializeAll() function will be performed only once at begin of the game:

          pPos = &CtrlCraneTestPosition.DatiPosition;
          
          // initialise data for test position crane:
          pPos->Distance.MinX = -22 -200;
          pPos->Distance.MaxX = -22 +200;
          pPos->Distance.MinY = -100;
          pPos->Distance.MaxY = +100;
          pPos->Distance.MinZ = 87 - 200;
          pPos->Distance.MaxZ = 87 + 200;

          
          pPos->Orienting.OrientHMin = -1024;
          pPos->Orienting.OrientHMax = +1024;
          pPos->Orienting.OrientVMin = 0;
          pPos->Orienting.OrientVMax = 0;
          pPos->Orienting.OrientRMin = 0;
          pPos->Orienting.OrientRMax = 0;

          CtrlCraneTestPosition.Dynamic=0;
          CtrlCraneTestPosition.Flags = TPOS_TURN_FACING_90; // TPOS_ flags

About the setting of Flags field with the TPOS_TURN_FACING_90 flag, it has been necessary to fix a problem about lara's alingment.
We'll see better this matter when we'll work on self-alignment of lara.

We used a new local variable defined in InitializeAll():

          StrTestPosition *pPos;

The data it's pratically the same of TestPosition script command.
We type a range of values for each value. For instance, for distance we added +/- 200 as tollerance, while with Orienting we used tollerance +/-1024 only for Horizontal difference facing.
About ideal values, we have rounded the horizontal orienting because in the log was -380 but this means we wished reach one of four hortogonal angles, so in this case 0

Note: there is a field of CtrlCraneTestPosition structure that we are not set: the CtrlCraneTestPosition.Slot field.
We should insert the slot of item, the control panel of the crane, but we don't know yet this slot.
For this reason we should remember to insert a valid value just we discovered the slot where it has been stored the control panel of the slot.
We discover the assigned value in InitSlotCtrlPanelCrane() function, so we set the slot value in this function:

          SlotCtrlCrane=FIND.SlotAssigned;
          // assign in CtrlCraneTestPosition structure the slot of control panel:
          CtrlCraneTestPosition.Slot = SlotCtrlCrane;



The CheckPositionAlignment() function

The data in global structure CtrlCraneTestPosition, will be passed to the CheckPositionAlignment() function.
This function has following prototype:

bool CheckPositionAlignment(StrTestPositionCmd *pTestPosition, int ObjectIndex);

It receives as arguments:
  1. pPosition
    The pointer of a StrTestPositionCmd structure. We'll pass the address of CtrlCraneTestPosition global variable

  2. ObjectIndex
    The index of object whose we wish check the alignment with Lara
    In our case, it wil be the index of control panel.

The function will discover if lara is aligned with pItem in right way respect the data of pPosition.
If she is aligned, it returns "true", otherwise "false".
We'll perform this call in Collision() procedure, only when lara is enough closed to the control panel, to avoid to waste cpu time (performing a complicated comparation also when she was very far).

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

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

          if (CheckPositionAlignment(&CtrlCraneTestPosition, ItemIndex)== true) {
                    // lara is aligned...
          }

Above code is not yet complete, of course, we need of more conditions to engage the driving crane mode.
It's necessary that it was true that:

When it's necessary type many conditions, it's better testing first the condition that requires less time to be performed, and only at end, the conditions requiring more cpu time.
In this way, everytime one (fast) condition is false, we'll skip the check for other condition more expensive as cpu time.

So, adding these other conditions the code will become:

void CollisionCtrlPanelCrane(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision)
{

          Get(enumGET.ITEM, ItemIndex, 0);

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

          Get(enumGET.INPUT,0,0);
          if (GET.Input.GameCommandsRead & enumCMD.ACTION) {
                    // it has been sent ACTION command

                    if (pLara->StateIdCurrent == 2 && pLara->AnimationNow == 103) {
                              // lara is in stand-up position (stateid=2 animation=103)
                              Get(enumGET.INFO_LARA,0,0);
                              if (GET.LaraInfo.TestFreeHands == true) {
                                        // lara has free hands
                                        if (CheckPositionAlignment(&CtrlCraneTestPosition, ItemIndex)== true) {
                                                  // lara is aligned...
                                                  // only as temporary test: flash the screen with red light to verify when alignment is good
                                                  PerformFlipeffect(NULL, 355, 0, 10);
                                        }
                              }
                    }
          }

In this code we had performed a flipeffect (355) to flash with red light when the alignment is fine.
This is a nice trick to check in game if alignment data we used are really good, or it's better change them a bit.
Now build the library, update and try in game.
We can move lara closed to the control panel in different position, and click ACTION to verify if in that position it will be seen as "aligned" correctly.

Note: About the condition "pLara->AnimationNow == 103", we used in direct way a control with value 103. Why in this case have not we used the GetRelativeAnimation() function to have a valid animation number?

if (GetRelativeAnimation(pLara) == 103)

Because the GetRelativeAnimation() it's necessary to convert the animation number of all items, with only exception of Lara.
The reason is that lara slot is always the first in the wad, and so there are NO other animations of previous items to mess the situation, like, differently, it happens for all other moveables.


How to engage the Crane

Testing in game the alignment data seem working fine.
So, now we have to replace that flipeffect for red light, with real activation of "driving crane" mode.
Once the game it will be in "driving crane" mode, the input commands, LEFT, RIGHT, ACTION ect, should not affect lara but only the crane.
To reach this target we have to persuade tomb raider engine that the crane was a vehicle.

How to enable a vehicle

To get this, we have to change the value of VehicleIndex global variable


VehicleIndex
In this global variable there is the item index of vechicle that lara is driving, or -1 if lara is not driving any vechicle.
So we'll type in this variable the index of the crane item.
To write a value in this variable we have to use this code:

*Trng.pGlobTomb4->pAdr->pVehicleIndex = ItemIndex;

We use the "*" sign at begin because it is a pointer to a single variable (and not a structure) to force to write (or read) to (or from) that address.
Note: you can find VehicleIndex also calling the Get() function in this way:

          Get(enumGET.INFO_LARA,0,0);
          GET.LaraInfo.IndexOfVehicle

But above code works only on reading, when you wish discover what is (or if there is) the vehicle that lara is driving.
You cann't use this GET. variable to change the value of vechicle.


The Crane_EngageDriving() function

So now we make a function to engage the driving of the crane.
We can name it "The Crane_EngageDriving", and in the code of CollisionCtrlPanelCrane() function, we replace that flipeffect for red light, with the call of Crane_EngageDriving():

                                        // lara has free hands
                                        if (CheckPositionAlignment(&CtrlCraneTestPosition, ItemIndex)== true) {
                                                  // lara is aligned...
                                                  // engage the driving of the crane
                                                  Crane_EngageDriving(ItemIndex);
                                        }

And then we type the code for Crane_EngageDriving() function:

// globals: already set in GET.pItem the item structure of control panel
void Crane_EngageDriving(short ItemIndex)
{
          int IndexOfCrane;

          // type as vehicle the index of the crane that we find in Ocb field of control panel:
          IndexOfCrane = GET.pItem->OcbCode;
          // since it is a NGLE index, we have to convert it to tomb index
          IndexOfCrane = FromNgleIndexToTomb4Index(IndexOfCrane);
          // verify if it is a valid item index
          if (IndexOfCrane==-1) {
                    SendToLog("ERROR: unvalid value as swinging crane index set in OCB field of control panel for crane");
                    // quit
                    return;
          }
          // set the index of crane to move
          *Trng.pGlobTomb4->pAdr->pVehicleIndex = IndexOfCrane;
          *Trng.pGlobTomb4->pAdr->pFlagsLaraHands = enumFLH.IS_GRABBING;
          // trigger that swiging crane
          // using Action trigger 43 ("Trigger. (Moveable) Activate <#>Object with (E)Timer value")
          PerformActionTrigger(NULL, 43, IndexOfCrane, 0);
          // set animation for lara: 445
          ForceAnimationForLara(445,-1);
}

In above code we can see as we read the ocb value of control panel to discover the index of the crane to move:

          // type as vehicle the index of the crane that we find in Ocb field of control panel:
          IndexOfCrane = GET.pItem->OcbCode;
          // since it is a NGLE index, we have to convert it to tomb index
          IndexOfCrane = FromNgleIndexToTomb4Index(IndexOfCrane);

Then we placed that index in VehicleIndex to enable the "vehicle" mode in tomb raider engine.

          *Trng.pGlobTomb4->pAdr->pVehicleIndex = IndexOfCrane;

We had set that lara has not free hands, now.

          *Trng.pGlobTomb4->pAdr->pFlagsLaraHands = enumFLH.IS_GRABBING;

We have also triggered the crane to move to avoid the risk it has not yet been enabled in game:

          // trigger that swiging crane
          // using Action trigger 43 ("Trigger. (Moveable) Activate <#>Object with (E)Timer value")
          PerformActionTrigger(NULL, 43, IndexOfCrane, 0);

And at end, we set the animation where lara move her hands to touch the control panel.
The animation is the number 445:

          // set animation for lara: 445
          ForceAnimationForLara(445,-1);


The frozen Lara

Testing in game last changes we see that, when we engage the control panel, lara stops any movement, netiher the animation 445 it has been performed.
It happens because, once we typed a value in VechicleIndex different than -1, the game disables all changes on lara: no game input will move lara and neither her current animations it will be performed.
Now we have to handle the crane and also remembering to animate lara to perform her animations.
This job it will be performed in ControlCraneManual() function...

How to detect the activation of the Crane

The code of control panel did its job, now it's the moment of swinging crane code to understand if it has been enabled and, if it is, managing all stuff.
The crane will discover if it has been enabled checking the value in VehicleIndex: if that value is the same of its own index, this means that it has to handle the "driving crane" mode.
So in ControlCraneManual() function we start checkig if current crane is the curren vehicle:

void ControlCraneManual(short ItemIndex)
{

          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle != ItemIndex) {
                    // current vehicle is NOT the current crane: quit
                    return;
          }

          // current crane it has been engaged..


Now the crane code should handle following issues:
  1. Read game commands and move the crane as required by the player (lara)

  2. When there is the command to move down the crane, it should verify if it is possible grabbing some item, or destroy it (like shatter) or killing him (like a baddy)

  3. The code should remember if currently it is grabbing and moving some item, or it has free jaws.

  4. When there is the command to leave the grabbed item, it should put it on the floor, or verifying if under the item there is another item to destroy or to kill.

  5. Animate Lara to perform her animations



How to animate lara during a vechicle phase

Since the animation of lara is the easier operation, we could begin from this.
To animate lara it is necessary using the AnimateItem() function.
So we add this code, to see if also in game the animation 445 it will be finally performed.
AnimateItem() function has only one argument: the pointer structure of moveable item to animate.

          // current crane it has been engaged...
          AnimateItem(GET.pLara);

Now build the library, update in trle folder and let see if now the animation of Lara works...




Ok the animation now works...
Now we have to handle game input but first to type this code we have to choose what are these commands...

The game commands to drive the crane

It is deducible that LEFT, RIGHT, FORWARD(UP), BACKWARD(DOWN) will be used to choose the direction of the crane in horizontal way, but we have to set also other commands:

In some circustances, we could use same game command for different targets, just there was no conflicts.
For instance, when the crane is moving vertically we could use UP / DOWN to move the crane up or down, in spite we used these commands also to move horizontally the crane.
To describe the commands we'll use common lexicon of tomb raider setting, in spite the meaning of commands in this case it will be different of course:

ACTION(ctrl) : Keeping down the action key, the FORWARD(UP)/ BACKWARD(DOWN) commands will be used to move up/down the crane slowly
DRAW WEAPON (space): Open the jaws and left falling down the grabbed item. If currently there is no grabbed item, nothing it will be performed
JUMP(alt): Left falling down the crane but this command is not available when the crane is grabbing an item.
WALK(shift): Try to close the jaws to grab some item.
ROLL(end): Quit the driving crane mode, and returns to common game playing

In this way we reduced a bit the number of different commands to manage, anyway the number is not so little, therefor we'll have to insert in some description the list.
The better side is the description of crane OCB, where we'll describe the ocb 2 to enable the "drivable crane" mode.
We'll type the ocb description only when we completed this exercise.

Global Variables used by drivable Crane

We need to use some global variable, fields of crane structure, to store some particular informations.


We can see that, the Reserved_34 and Reserved_36 are the same and with same menaning of swinging crane in automatic mode.
Now we added the Reserved_38 field to store the index of grabbed item.
We should always remember to initialise a value in all global variable we'll use...
So we go in InitialiseCrane() function and we'll add the initalization of this other field:

          // inizialize global variables
          GET.pItem->Reserved_34 = GET.pItem->OrientationH; // current horizontal direction
          GET.pItem->Reserved_36 = 0; // vertical speed
          GET.pItem->Reserved_38 = -1; // index of grabbed item
          GET.pItem->Reserved_3A =0; // flags for manual crane


How to handle horizontal movements of the Crane

Since we have to do a lot of stuff, we should work gradually to face differnet logic units of crane code.
We begin with the code to handle horizontal movements of the crane.

void ControlCraneManual(short ItemIndex)
{
          int BaseCeilingY;
          bool TestChanged;
          short OldDirection;
          int IncX;
          int IncZ;
          int SaveCeilingHeight;

          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle != ItemIndex) {
                    // current vehicle is NOT the current crane: quit
                    return;
          }

          // current crane it has been engaged...
          AnimateItem(GET.pLara);

          // discover if the crane is at center of current sector.
          // only in this situation we could accept many game commands to change direction, open/close jaws, move up/down ect.
          Get(enumGET.ITEM,ItemIndex,0);

          // ask for currnet input commands
          Get(enumGET.INPUT,0,0);

          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);

          BaseCeilingY = FLOOR.CeilingHeight + 1200;

          // save ceiling height, since IsCraneFeeeDirection() will change FLOOR data

          SaveCeilingHeight = FLOOR.CeilingHeight;

          // saved current (old) direction to discover if then it has been changed
          OldDirection= GET.pItem->Reserved_34;

          if (FLOOR.SectorCoords.Radius < 15) {
                    // Crane is at center of current sector.
                    // now we can perform some game commands
                    // variable to remember it has been chosen a new direction
                    TestChanged=false;
                    // now we check for horizontal moving.
                    // they will work only when ACTION command is not present, and the crane is not moving vertically
                    if ((GET.Input.GameCommandsRead & enumCMD.ACTION) == 0 &&
                              GET.pItem->CordY == BaseCeilingY &&
                              GET.pItem->Reserved_36 == 0) {
                              // there is no ACTION key hitten, crane is hanged on the ceiling and there is no vertical movement in progress

                              if (GET.Input.GameCommandsRead & enumCMD.LEFT) {
                                        // left: moving to west
                                        TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.WEST, SaveCeilingHeight);
                              }

                              // now try other directions only if it has not yet been changed direction
                              if ((GET.Input.GameCommandsRead & enumCMD.RIGHT) != 0 && TestChanged==false) {
                                        // right: movign to east
                                        TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.EAST,SaveCeilingHeight);
                              }
                              if ((GET.Input.GameCommandsRead & enumCMD.UP) != 0 && TestChanged==false) {
                                        // up(forward): moving to north
                                        TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.NORTH, SaveCeilingHeight);
                              }
                              if ((GET.Input.GameCommandsRead & enumCMD.DOWN) != 0 && TestChanged==false) {
                                        //down(backward): moving to south
                                        TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.SOUTH,SaveCeilingHeight);
                              }
                    }
                    // if we have not changed direction, check if old direction is good to continue
                    if (TestChanged==false && (GET.pItem->Reserved_3A & CRANE_MOVING_HORIZONTALLY)!=0) {
                              if (IsCraneFreeDirection(ItemIndex, GET.pItem->Reserved_34,SaveCeilingHeight)==false) {
                                        // stop the horizontal movement
                                        // remove the CRANE_MOVING_HORIZONTALLY flag
                                        GET.pItem->Reserved_3A &= ~CRANE_MOVING_HORIZONTALLY;
                              }
                    }
          }
          // move the crane following current horizontal and vertical speed
          if (GET.pItem->Reserved_3A & CRANE_MOVING_HORIZONTALLY) {
                    // move the crane horizontally
                    GetIncrements(GET.pItem->Reserved_34, &IncX, &IncZ, 16);
                    GET.pItem->CordX += IncX;
                    GET.pItem->CordZ += IncZ;
                    // update position in the case it changed room
                    UpdateItemRoom(ItemIndex);
                    // update height floor field
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;
                    // if crane has just changed its direction, we peform a different sound
                    if (OldDirection == GET.pItem->Reserved_34) {
                              // it's the same direction: usual moving sound
                              SoundEffect(GetSFX(SFX_CRANE_MOVING, 309), &GET.pItem->CordX, 0);
                    }else {
                              // in this moment crane changed direction: using a metallic sound to signal this steering
                              // 312 BLADES_CLASH_LOUD
                              SoundEffect(312, &GET.pItem->CordX,0);
                    }
          }
}

When the ACTION key is hit, the direction commands will work only for vertical movments, so this means that we'll have to detect that the ACTION key was not hit, to handle the horizontal movments.

                    if ((GET.Input.GameCommandsRead & enumCMD.ACTION) == 0 &&
                              GET.pItem->CordY == BaseCeilingY &&
                              GET.pItem->Reserved_36 == 0) {
                              // there is no ACTION key hitten, crane is hanged on the ceiling and there is no vertical movement in progress

We had also other two conditions:
- The crane has to be hanged on the ceiling and this happens when its Y coordinate is default higher position of BaseCeilingY (CeilingY of the room + 1200 game units)
- The crane has neither started a vertical movement and we discover this checking the value of Vertical Speed stored in Reserved_36 field.

We need of a function to check if it is possible moving where the player wished, we called this function "IsCraneFreeDirection":

                              if (GET.Input.GameCommandsRead & enumCMD.LEFT) {
                                        // left: moving to west
                                        TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.WEST, SaveCeilingHeight);
                              }

We saved result in TestChanged local variable, to remember if we had already found a valid direction. If we did, it's not necessary check also for other commands and directions:

                              // now try other directions only if it has not yet been changed direction
                              if ((GET.Input.GameCommandsRead & enumCMD.RIGHT) != 0 && TestChanged==false) {
                                        // right: movign to east
                                        TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.EAST,SaveCeilingHeight);
                              }


If the player send no command, or no valid direction, we have to check if going on same old direction there is some obstacles:

                    // if we have not changed direction, check if old direction is good to continue
                    if (TestChanged==false && (GET.pItem->Reserved_3A & CRANE_MOVING_HORIZONTALLY)!=0) {
                              if (IsCraneFreeDirection(ItemIndex, GET.pItem->Reserved_34,SaveCeilingHeight)==false) {
                                        // stop the horizontal movement
                                        // remove the CRANE_MOVING_HORIZONTALLY flag
                                        GET.pItem->Reserved_3A &= ~CRANE_MOVING_HORIZONTALLY;
                              }
                    }

In above code we see a new constant we set to mean: moving horizontally the crane:

// flags for manual crane (stored in Reserved_3A field of crane structure)
#define CRANE_MOVING_HORIZONTALLY 0x0001

Using automatic crane (in previous exercise) it was not necessary, because that crane was always moving, but now with manual crane, when it moved in some impossible direction, it will stop.
To remember when we have to do NOT move it, we check the flag CRANE_MOVING_HORIZONTALLY: if it is missing, we'll skip the code to move the crane horizontally.

          // move the crane following current horizontal
          if (GET.pItem->Reserved_3A & CRANE_MOVING_HORIZONTALLY) {
                    // move the crane horizontally
                    GetIncrements(GET.pItem->Reserved_34, &IncX, &IncZ, 16);
                    GET.pItem->CordX += IncX;
                    GET.pItem->CordZ += IncZ;
                    // update position in the case it changed room
                    UpdateItemRoom(ItemIndex);
                    // update height floor field
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;

At end we added a diferent sound when the direction it has been just modified.
It was an homework for previous exercise, do you remember?

                    // if crane has just changed its direction, we peform a different sound
                    if (OldDirection == GET.pItem->Reserved_34) {
                              // it's the same direction: usual moving sound
                              SoundEffect(GetSFX(SFX_CRANE_MOVING, 309), &GET.pItem->CordX, 0);
                    }else {
                              // in this moment crane changed direction: using a metallic sound to signal this steering
                              // 312 BLADES_CLASH_LOUD
                              SoundEffect(312, &GET.pItem->CordX,0);
                    }


The IsCraneFreeDirection() function

The function to check if the crane is able to move in some horizontal direction is very alike than IsValidCeiling() function that we had used for automatic crane.
Really our new function used own that old function, but in this case we have to add some code more to handle new reserved field of automatic crane.

// discover if the crane can move on next sector in absolute direction Direction
// CurrentCeilingY is height of ceiling where is a currently crane (good value for ceiling)
// globals: GET.pItem is the crane

bool IsCraneFreeDirection(short ItemIndex, short Direction, int CurrentCeilingY)
{


          if (IsValidCeiling(Direction, CurrentCeilingY) == false) {
                    // ceiling or wall stop the crane in this direction
                    return false;
          }

          // ok: set new direction
          GET.pItem->Reserved_34 = Direction;
          // enable horizontal movement
          GET.pItem->Reserved_3A |= CRANE_MOVING_HORIZONTALLY;
          return true;

}

The most of control code is based on old IsValidCeiling() function, but then, when it's possible moving in that direction, the code we'll set new direction in Reserved_34 field, and the flag to enable the horizontal movement it will be added to Reserved_3A field.

Now we build the project and verify how it works in game...




Ok, it works...
We can move the crane in all hortogonal directions and when the crane is moving outside of valid ceiling positions, it will stop.

The crane is moving correctly horizontally but before going on, we should solve immediatly an issue...
The view of the crane is not always very clear. When it moved in outer limits (at left, right but also moving too closed to lara) it's not easy seeing it and what there is below of it.
This happens because the camera mode is yet on Lara but now it's not lara the leading actor but the crane, that is in a very different position.
For this reason we should find a way to use the crane as camera target.
We can use different stuff to realize this target, anyway the problem is that we cann't solve this issue in hardcoded way, because our crane could be in different levels with different rooms, while our code is not able to understand the better position and features of this camera.
So the solution is to let that was the level builder to enable right camera mode, with best settigs for its level.

The problem now is about how to allow to level builder to discover the right moment when enable this new camera mode.
Ok we took on a tough task, anyway there is a way to realize this target...


How to set a new global trigger event

When we create some hardcoded feature, like our drivable crane, it should be better creating also some new global trigger evet (GT_) to singal when different hardcoded situation begun or just quit.
To signal a new global trigger event, you have to declare a new GT_ constant, with a value different of any other.

WARNING: the GT_ constants are not private (locals) but shared (globals), for this reason it's important you choose a value different, and better very far, from other values (used for GT_ constants) by other engines: tomb next generation, and other plugins.
It's always an obligation of last (more up to date) plugin author to avoid conflicts with older engines. So when you are building your plugin you should discover all plugin already built, download them, discover what range of values they used (for instance with GT_ constants) and then choose a new range different by all others.
Note: this speech is true for GT_ constants but not for CUST_, PARAM_ and OBJ_ constants, since these are private (locals) and their values cann't conflict with those of other engines.

About range of values for GT_ constant there is a lot of chances: currently trng used only first 40 values (1-40) for GT_ constants but valid values are between 1 upto 32767, so it's only a question to choose a very far range of values and there will be space for alls.
In our case we'll set as first value for our GT_ constants the value 300.

// constant for GT_ new global trigger events
#define GT_DRIVING_CRANE_START 300
#define GT_DRIVING_CRANE_QUIT 301

So we define these two constants in our "Constants_mine.h" source, but then we have to declare same values (with some description) also in .script file to get them visible also in NG_Center program:

GT_DRIVING_CRANE_START:300 ;used with GlobalTrigger= command>With this global trigger you can detect when the driving crane begun in game.>You need to type in Parameter fied the index of the Swinging Crane item whose you want detect the driving mode.>Note: the global trigger it will be engaged when the first animation of lara over the Crane Control Panel it has been just completed.

GT_DRIVING_CRANE_QUIT:301 ;used with GlobalTrigger= command>When lara quits the driving of the crane it will be engaged this global trigger.>Remember to type in Parameter field the index of the crane whose you wish detect the end of driving.

We set that the global event GT_DRIVING_CRANE_START will start only when the animation used to show lara is engagin the crane it has been completed. The reason is because probably the level builder will use this global trigger to set a different camera mode but if we engaged immediatly the global trigger, the camera changed its target before showing the first animation of lara and it should be a pity.


How to engage in the code a GT_ event

To send a global trigger event, from our plugin, we'll use this function:

bool DetectedGlobalTriggerEvent(int GT_Event, int Parameter, bool TestIgnoreParameter)

First parameter it will be the GT_ constant that identifies the event just happened.
Second parameter it will be the further parameter required in GlobalTrigger script command, linked with this GT_ global trigger.
Third parameter it will be "true" or "false" and it used to inform if the Parameter (second argument) should be really used or less, since not all global trigger requires also a given parameter to be engaged.

So now we'll add a call to DetectedGlobalTriggerEvent() function, when the crane it has been engaged and the first animation it has been just completed.

Our code to signal the begin of driving mode

With that matter to send the GT_DRIVING_CRANE_START event only when the first hardcoded animation (the 445) we got another complication.
The problem is that for event like the "start" or the "quit" of some event, it should be better send only one event at start, and not a lot of event for all durate of that event (in our case: the driving phase of the crane).

So we'll detect when the animation 445 it has been completed, and this it's easy, but also to avoid to send multiple GT_DRIVING_CRANE_START events.
We can use a new flag for our manual crane to remember when you just sent that event.

// flags for manual crane (stored in Reserved_3A field of crane structure)
#define CRANE_MOVING_HORIZONTALLY 0x0001
#define CRANE_START_DRIVING 0x0002

So we added another CRANE_ flag.
Then we'll add following code to begin of ControlCraneManual() function:

          Get(enumGET.ITEM,ItemIndex,0);

          if ((GET.pItem->Reserved_3A & CRANE_START_DRIVING)==0) {
                    // but the driving is not yet begun
                    // verify if the first hardcoded animation it has been completed
                    if (GET.pLara->AnimationNow != 445) {
                              // animation of lara is different: it has been completed, now we begin the driving
                              GET.pItem->Reserved_3A |= CRANE_START_DRIVING;
                              // and only in this frame we send the GT_DRIVING_CRANE_START event
                              // convert the index of the crane in ngle format:
                              IndexNgle= FromTomb4IndexToNgleIndex(ItemIndex);
                              // send the event
                              DetectedGlobalTriggerEvent(GT_DRIVING_CRANE_START, IndexNgle, false);
                    }
          }
          // if the driving it's not yet engaged we quit the code:
          if ((GET.pItem->Reserved_3A & CRANE_START_DRIVING)==0) {
                    // we are yet waiting that the first animation of lara was completed...
                    return;
          }

The new CRANE_START_DRIVING flag, will signal when the driving begins. It will happen only when lara completed its first 445 animation.
When it happened, we add the CRANE_START_DRIVING flag to remember to have begun the driving, and we send the GT_DRIVING_CRANE_START event.
We had to supply also the index of the crane but first we have to convert it to ngle format, because in the script the level builder typed that index surely in ngle format.
Then we set "false" third argument (TestIgnoreParameter) because in this event the Parameter is important, it contains the index of the crane.

                    if (GET.pLara->AnimationNow != 445) {
                              // animation of lara is different: it has been completed, now we begin the driving
                              GET.pItem->Reserved_3A |= CRANE_START_DRIVING;
                              // and only in this frame we send the GT_DRIVING_CRANE_START event
                              // convert the index of the crane in ngle format:
                              IndexNgle= FromTomb4IndexToNgleIndex(ItemIndex);
                              // send the event
                              DetectedGlobalTriggerEvent(GT_DRIVING_CRANE_START, IndexNgle, false);
                    }

Then we skipped all code in the case the driving is not yet started, beacuse lara is yet performing its engagin of the crane using the control panel (animation 445):

          // if the driving it's not yet engaged we quit the code:
          if ((GET.pItem->Reserved_3A & CRANE_START_DRIVING)==0) {
                    // we are yet waiting that the first animation of lara was completed...
                    return;
          }

To avoid problems, we should remember to clear the CRANE_START_DRIVING when we quit the driving of the crane, otherwise when lara will use same crame in second time, that flag will be yet present and we'll have problem with next activation.
So we add the code to detect the command to quit the driving crane and we send also the GT_ event:

          if (FLOOR.SectorCoords.Radius < 15) {

                    // Crane is at center of current sector.
                    // now we can perform some game commands
                    if (GET.Input.GameCommandsRead & enumCMD.ROLL) {
                              // command ROLL(end): quit driving crane:
                              Crane_QuitDriving(ItemIndex);
                              return;
                    }

In the code of ControlCraneManual() function we typed above code to detect when player hit ROLL(end) command.
Now we type the new function to handle the quit of driving crane mode:

// globals: already set GET.pItem with crane structure
void Crane_QuitDriving(short ItemIndex)
{
          int IndexNgle;

          // remove CRANE_START_DRIVING flag, otherwise next time we engage the crane it will be yet present
          GET.pItem->Reserved_3A &= ~CRANE_START_DRIVING;
          // set free hands for lara
          *Trng.pGlobTomb4->pAdr->pFlagsLaraHands = enumFLH.ALMOST_FREE_HANDS;
          // disable vechicle:
          *Trng.pGlobTomb4->pAdr->pVehicleIndex=-1;
          // set animation to come back to common stand-up position
          ForceAnimationForLara(447, -1);
          // set event for quit of driving
          IndexNgle=FromTomb4IndexToNgleIndex(ItemIndex);
          DetectedGlobalTriggerEvent(GT_DRIVING_CRANE_QUIT, IndexNgle, false);
}

Now we test if the code works fine about coming-in/out of driving phase....


A bit of scripting to have right camera view for the Crane

In spite this operation is all about scripting and not plugin code, we have to verify if our new GT_ events work fine togheter with the script.
So now we build a triggergroup to enable a camera that follows the crane and then another triggergroup to quit previous camera mode.
We'll link these two triggergroups with GT_DRIVING_CRANE_START and GT_DRIVING_CRANE_QUIT to verify it the camera mode starts and ends in right time.

; Set Trigger Type - ACTION 42
; Exporting: TRIGGER(42:0) for ACTION(118) {Tomb_NextGeneration}
; <#> : ANIMATING2 ID 118 in sector (6,5) of Room23
; <&> : Trigger. (Target) Set <#>Moveable as Target for camera or fixed camera
; (E) :
; Values to add in script command: $5000, 118, $2A


; Set Trigger Type - ACTION 41
; Exporting: TRIGGER(22825:0) for ACTION(137) {Tomb_NextGeneration}
; <#> : CAMERA_FIXED ID 137 in sector (6,8) of Room23
; <&> : Trigger. (Camera) Activate <#>Camera or fixed camera with (E)Timer value
; (E) : Timer= 1
; Values to add in script command: $5000, 137, $0129

; Set Trigger Type - FLIPEFFECT 406
; Exporting: TRIGGER(1:0) for FLIPEFFECT(406) {Tomb_NextGeneration}
; <#> : Camera. Get/Remove<&> infinite durate for current camera (not flyby)
; <&> : Enable infinite durate
; (E) :
; Values to add in script command: $2000, 406, $1

; Set Trigger Type - FLIPEFFECT 406
; Exporting: TRIGGER(0:0) for FLIPEFFECT(406) {Tomb_NextGeneration}
; <#> : Camera. Get/Remove<&> infinite durate for current camera (not flyby)
; <&> : Remove infinite durate, quit now the camera
; (E) :
; Values to add in script command: $2000, 406, $0

;triggergroup to start crane camera
TriggerGroup=          1, $5000, 118, $2A, $5000, 137, $0129, $2000, 406, $1

;triggergroup to quit crane camera
TriggerGroup= 2, $2000, 406, $0

;global trigger to enable crane camera:
GlobalTrigger=          1, IGNORE, GT_DRIVING_CRANE_START, 118, IGNORE, 1, IGNORE
;global trigger to disable crane camera:
GlobalTrigger=          2, IGNORE, GT_DRIVING_CRANE_QUIT, 118, IGNORE, 2, IGNORE

Now we build the script and try how it works...

The new Crane Camera



It works fine...
Now we can follow the crane in any position, also in outer limits of the room, like two corners or when it was over lara at middle.

Moving vertically the Crane

Now, that we are able to see fine the crane in any position, we can type the code to move up/down the crane.
This function is very alike than Crane_MoveUpDown() function, but with drivable crane it's a bit more complicated.
While automatic crane had only one vertical speed to move down (falling with simulation of gravity), in drivable crane we have the same gravity fall but more, the moving down slowly.
Since with drivable crane it's possible that player move up the crane while it was moving down, this inversion of direction it will be simulated with a slow vertical speed at begin.
Then we'll have the matter about items to lift up, but now we begin to type the code to move up/down the crane.

A flow-chart to plan the vertical movements of the Crane

If you are not a programmer, probably you don't know yet the flow-charts.
A flow-chart is a way to plan some generic project and it is particularly suggested to plan decisions.
Since a program it's a list of condition (to set a decision) and operations (the final result by decisions), the flow-chart are very often used to plan a code to realize some target.
More complicated is the target and more useful will be the planning with flow-charts.
A flow-chart (like the image showed at left) is a mix of geomtric shapes where you type some text to describe their meaning.
The rhombus is to describe a condition. Inside you type the condition and at its vertices there will be lines with arrows labeled with "true" or "false" (but it used also "yes" and "no", of course).
If the condition is "true" you follow the line marked with "true" to write what it will be performed when that condition is right, while the line marked "false" it will show you what it will be perfomed when the condition was false.
The operations are inside of common rectangles (or squares, there is no difference).
About other shapes, there is a rounded rectangle that used only to set the start and the end of your planning.
In the left image you can see an example of flow-chart to describe the management of commands to move up/down the crane.
You should write the flow-chart before writing really the code, in this way you can organize better you project and then it will be easier writing the code, following the flow-chart logic.
Anyway it's possible also doing the opposite: it happens when you wish explain to other people the general logic of your code since a flow-chart is also a good way to get easier to be understood a complicated code.
For instance looking left image, we can discover many details about the logic of up/down movements:



You can use the wished level of detail for the operations and conditions you write.
I mean that, you can draw a flow chart where in any shape there is a low-level operation: like "A = B+2;", or a very general flow-chart, where you wish only organize in general way a project, in this case in some rectangle you write an operation like "To do move the Crane vertically", that is very generic but it could be useful when you wish make a flow-chart for all code of the crane and not only about a single little part, as I did in this case
I used a free program "Diagram Designer" by MeeSoft, to make above diagram. It's enough nice, in spite I've not yet learnt to use it very fine.
Anyway you can use also only pen and paper and probably for first attempt it's better: more easy and fast.
Probably in next exercise I'll use newly flowchart to show you the logic of some complicated code.

The code to manage the vertical movements of the Crane

Once we saw the flow-chart about the managament of vertical movements, now we see the low-level code.
This is the code that follow the logic of above flow-chart:

          if (FLOOR.SectorCoords.Radius < 15) {
                    // Crane is at center of current sector.
                    // now we can perform some game commands
                    // since when there is ACTION(ctrl) down the direction will work only to move up/down the crane
                    // we analyse the two situations: when the CTRL key is down, and when is not
                    // note: we cann't accept commands to change vertical movements if the crane is falling down

                    if ((GET.Input.GameCommandsRead & enumCMD.ACTION) !=0 &&
                              (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN) == 0) {
                              // ACTION is down: only up/down like directions, to move up/down the crane

                              if (GET.Input.GameCommandsRead & enumCMD.DOWN) {
                                        // ---------- MOVE DOWN SLOWLY THE CRANE -----------------
                                        // set a vertical speed to do move down the crane. set a positive value.
                                        // if previous vertical speed was 0 or yet positive, we set immediatly the standard vspeed


                                        // while if it was negative (the crane was moving up a moment ago) to simulate the inversion of
                                        // direction, we place a very little speed and we'll increase it until reach standard speed in
                                        // next frame
                                        if (GET.pItem->Reserved_36 >= 0) {
                                                  // standard speed to move down (slowly)
                                                  GET.pItem->Reserved_36 = 16;
                                        }else {
                                                  // the crane was moving up, and now there is the command to invert direction
                                                  // we begin with very little speed
                                                  GET.pItem->Reserved_36 = 1;
                                        }
                              }
                              if (GET.Input.GameCommandsRead & enumCMD.UP) {
                                        // -------- MOVE UP SLOWLY THE CRANE --------------------
                                        // if the crane already reached the ceiling we need to ignore this command
                                        // and force the baseceiling y
                                        if (GET.pItem->CordY <= BaseCeilingY) {
                                                  // crane reached ceiling: stop here
                                                  GET.pItem->CordY = BaseCeilingY;
                                                  GET.pItem->Reserved_36 =0;
                                        }else {
                                                  // crane has not yet reached the ceiling
                                                  // change vertical speed
                                                  // verifying if previously the crane was moving in opposite direction:
                                                  if (GET.pItem->Reserved_36 > 0) {
                                                            // it was moving down, and now up: set a little vertical speed
                                                            GET.pItem->Reserved_36 = -1;
                                                  }else {
                                                            // it was still or moving up: set standard speed
                                                            GET.pItem->Reserved_36 = -16;
                                                  }
                                        }
                              }
                    }
                    // verify if there i command to fall down the crane.
                    // JUMP command but also crane has to be hanged on the ceiling
                    if ((GET.Input.GameCommandsRead & enumCMD.JUMP)!=0 &&
                              GET.pItem->CordY == BaseCeilingY){
                              // command to left fall down the crane while it hanged on the ceiling
                              // but if we are already in this pahse: ignore the command
                              if ((GET.pItem->Reserved_3A & CRANE_FALLING_DOWN)==0) {
                                        // begin slowly and then it will be icnreased fastly
                                        GET.pItem->Reserved_36 = 1;
                                        GET.pItem->Reserved_3A |= CRANE_FALLING_DOWN;
                              }
                    }

                    // if it has been engaged a vertical movement, remove the flag for horizontally moving.
                    if (GET.pItem->Reserved_36 != 0) {

                              GET.pItem->Reserved_3A &= ~CRANE_MOVING_HORIZONTALLY;
                    }
          }

In above code I removed the code to move horizontally to show only the side for vertical movement management.
As you can see in the code there are more details and also some condition more than those showed in flow-chart.
For instance there is a variable vertical speed in according with previous direction: when the direction is suddenly inverted, the start in new direction it will happen slowly at begin.

                                                  // change vertical speed
                                                  // verifying if previously the crane was moving in opposite direction:
                                                  if (GET.pItem->Reserved_36 > 0) {
                                                            // it was moving down, and now up: set a little vertical speed
                                                            GET.pItem->Reserved_36 = -1;
                                                  }else {
                                                            // it was still or moving up: set standard speed
                                                            GET.pItem->Reserved_36 = -16;
                                                  }

It has been created another flag to signal when the crane it has been let falling down: the CRANE_FALLING_DOWN flag.

#define CRANE_FALLING_DOWN 0x0004

The falling down (JUMP command) it will be accepted only if the crane is hanged at ceiling other conditons already showed in flow-chart:

                    // verify if there i command to fall down the crane.
                    // JUMP command but also crane has to be hanged on the ceiling
                    if ((GET.Input.GameCommandsRead & enumCMD.JUMP)!=0 &&
                              GET.pItem->CordY == BaseCeilingY){
                              // command to left fall down the crane while it hanged on the ceiling
                              // but if we are already in this pahse: ignore the command
                              if ((GET.pItem->Reserved_3A & CRANE_FALLING_DOWN)==0) {
                                        // begin slowly and then it will be icnreased fastly
                                        GET.pItem->Reserved_36 = 1;
                                        GET.pItem->Reserved_3A |= CRANE_FALLING_DOWN;
                              }
                    }

To avoid that there was a mixign between horizontal and vertical movements, it has been cleared the flag to enable the horizontal movements, when there is a vertical speed:



                    // if it has been engaged a vertical movement, remove the flag for horizontally moving.
                    if (GET.pItem->Reserved_36 != 0) {

                              GET.pItem->Reserved_3A &= ~CRANE_MOVING_HORIZONTALLY;
                    }

After the management of input commands, it has been create a new functiont to apply the vertical movement: the Crane_MoveCraneVertically() function:

// globals: GET.pItem structrure of crane, FLOOR data of current sector
void Crane_MoveCraneVertically(short ItemIndex, int BaseCeilingY)
{
          short VSpeed;

          VSpeed = GET.pItem->Reserved_36;

          if (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN) {
                    // the crane should falling down
                    // we have to do gravity simulation: increase the Vertical speed until it reaches a max value
                    VSpeed += 8;
                    if (VSpeed > 128) VSpeed=128;

          }else {
                    // the crane should move up/down slowly with constant speed but if the speed is less than default +16/-16
                    // it means we have to simulate a gradual acceleration
                    if (VSpeed != 16 && VSpeed != -16) {
                              // increase or decrease following current sign
                              if (VSpeed < 0) {
                                        //negative speed: decrease
                                        VSpeed--;
                              }else {
                                        // positive: increase
                                        VSpeed++;
                              }
                    }
          }
          GET.pItem->CordY += VSpeed;
          // check if crane reached some obstacle:
          if (VSpeed < 0) {
                    // crane was moving up: check if it reached ceiling
                    if (GET.pItem->CordY <= BaseCeilingY) {
                              // yes: reached ceiling: stop the movement
                              VSpeed=0;
                              GET.pItem->CordY = BaseCeilingY;
                    }
          }else {
                    // crane was moving down
                    // check if crane reached the floor
                    if (GET.pItem->CordY >= FLOOR.FloorHeight) {
                              // yes: reached the floor: stop the movement
                              VSpeed=0;
                              GET.pItem->CordY = FLOOR.FloorHeight;

                              // effects to have touched the floor
                              ForceAnimationForItem(GET.pItem, 2, 0);
                              SoundEffect(GetSFX(SFX_CRANE_TOUCH_FLOOR, 314), &GET.pItem->CordX, 0);
                              if (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN) {
                                        // to do earthquake only if there was falling down
                                        // earth quake: flipeffect 1
                                        PerformFlipeffect(NULL, 1, 0, 0);

                                        // remove the CRANE_FALLING_DOWN flag
                                        GET.pItem->Reserved_3A &= ~CRANE_FALLING_DOWN;
                              }
                    }
          }                    
          GET.pItem->Reserved_36 = VSpeed;

          // check for change of room
          UpdateItemRoom(ItemIndex);
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          GET.pItem->HeightFloor = FLOOR.FloorHeight;
}

In above code the only handled obstacles are the ceiling, moving upstairs, and the floor, moving downstairs.

In ControlCraneManual() function, it has been added also a code to handle the different animation in according with direction of the crane: when it move up, the jaws will be close, while moving down, they will be open.

          // handle animations in according with further direction on vertical movments
          if (GET.pItem->Reserved_36 > 0) {
                    // crane is moving down: open the jaws
                    NumAnim = GetRelativeAnimation(GET.pItem);

                    switch (NumAnim) {
                    case 5:
                              // jaws already open: nothing to do
                              break;
                    case 0:
                              // crane has close jaws: set animations to do open them
                              ForceAnimationForItem(GET.pItem, 1, 1);
                              break;
                    case 6:
                              // crane is grabbing: if there is no grabbed item, force to open the jaws
                              if (GET.pItem->Reserved_38 == -1) {
                                        // jaws are empty, no grabbed item: force to open the jaws
                                        ForceAnimationForItem(GET.pItem, 4,1);
                              }
                              break;
                    default:
                              // all other cases: don't change anything while an animation of change jaws is in progress
                              break;
                    }
          }
          if (GET.pItem->Reserved_36 < 0) {
                    // crane is moving up: close the jaws
                    // at least it was not grabbing an item or the jaws are already close
                    NumAnim = GetRelativeAnimation(GET.pItem);
                    switch(NumAnim)
                    {
                    case 0:
                              // crane is already close: ok, nothing to do
                              break;
                    case 5:
                              // crane is open: close the jaws
                              ForceAnimationForItem(GET.pItem, 2, 0);
                              break;
                    case 6:
                              // crane is grabbing: check if it has really caught some item or less
                              if (GET.pItem->Reserved_38 == -1) {
                                        // jaws are empty, no grabbed item: force to open the jaws
                                        ForceAnimationForItem(GET.pItem, 4,0);
                              }
                              // when there is a grabbed item do not change animation
                              break;
                    default:
                              // crane is changing its jaws : do not change anything now to don't break an animation in the middle
                              break;
                    }
          }


The State-IDs for drivable Crane

In above code we used ForceAnimationForItem() setting also the next state id value.
The state-ids of different animations work to signal the current opening (or less) of crane's jaws:

0 = Closed Jaws
1= Open Jaws
2= Large Grabbing
3= Slim Grabbing


The code of ControlCraneManual() function

After having showed some parts of new changes, now we see the current complete code of ControlCraneManual() function:

void ControlCraneManual(short ItemIndex)
{
          int BaseCeilingY;
          bool TestChanged;
          short OldDirection;
          int IncX;
          int IncZ;
          int IndexNgle;
          int NumAnim;
          int SaveCeilingHeight;

          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle != ItemIndex) {
                    // current vehicle is NOT the current crane: quit
                    return;
          }
          // current crane it has been engaged...
          AnimateItem(GET.pLara);
          Get(enumGET.ITEM,ItemIndex,0);
          if ((GET.pItem->Reserved_3A & CRANE_START_DRIVING)==0) {
                    // but the driving is not yet begun
                    // verify if the first hardcoded animation it has been completed
                    if (GET.pLara->AnimationNow != 445) {
                              // animation of lara is different: it has been completed, now we begin the driving
                              GET.pItem->Reserved_3A |= CRANE_START_DRIVING;
                              // and only in this frame we send the GT_DRIVING_CRANE_START event
                              // convert the index of the crane in ngle format:
                              IndexNgle= FromTomb4IndexToNgleIndex(ItemIndex);
                              // send the event
                              DetectedGlobalTriggerEvent(GT_DRIVING_CRANE_START, IndexNgle, false);
                    }
          }
          // if the driving it's not yet engaged we quit the code:
          if ((GET.pItem->Reserved_3A & CRANE_START_DRIVING)==0) {
                    // we are yet waiting that the first animation of lara was completed...
                    return;
          }
          // ask for currnet input commands
          Get(enumGET.INPUT,0,0);
          // discover if the crane is at center of current sector.
          // only in this situation we could accept many game commands to change direction, open/close jaws, move up/down ect.
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          BaseCeilingY = FLOOR.CeilingHeight + 1200;
          SaveCeilingHeight = FLOOR.CeilingHeight;
          
          // saved current (old) direction to discover if then it has been changed
          OldDirection= GET.pItem->Reserved_34;
          if (FLOOR.SectorCoords.Radius < 15) {
                    // Crane is at center of current sector.
                    // now we can perform some game commands
                    if (GET.Input.GameCommandsRead & enumCMD.ROLL) {
                              // command ROLL(end): quit driving crane:
                              Crane_QuitDriving(ItemIndex);
                              return;
                    }
                    // since when there is ACTION(ctrl) down the direction will work only to move up/down the crane
                    // we analyse the two situations: when the CTRL key is down, and when is not
                    // note: we cann't accept commands to change vertical movements if the crane is falling down
                    if ((GET.Input.GameCommandsRead & enumCMD.ACTION) !=0 &&
                              (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN) == 0) {
                              // ACTION is down: only up/down like directions, to move up/down the crane


                              if (GET.Input.GameCommandsRead & enumCMD.DOWN) {
                                        // ---------- MOVE DOWN SLOWLY THE CRANE -----------------
                                        // set a vertical speed to do move down the crane. set a positive value.
                                        // if previous vertical speed was 0 or yet positive, we set immediatly the standard vspeed
                                        // while if it was negative (the crane was moving up a moment ago) to simulate the inversion of
                                        // direction, we place a very little speed and we'll increase it until reach standard speed in
                                        // next frame
                                        if (GET.pItem->Reserved_36 >= 0) {
                                                  // standard speed to move down (slowly)
                                                  GET.pItem->Reserved_36 = 16;
                                        }else {
                                                  // the crane was moving up, and now there is the command to invert direction
                                                  // we begin with very little speed
                                                  GET.pItem->Reserved_36 = 1;
                                        }
                              }
                              if (GET.Input.GameCommandsRead & enumCMD.UP) {
                                        // -------- MOVE UP SLOWLY THE CRANE --------------------
                                        // if the crane already reached the ceiling we need to ignore this command
                                        // and force the baseceiling y
                                        if (GET.pItem->CordY <= BaseCeilingY) {
                                                  // crane reached ceiling: stop here
                                                  GET.pItem->CordY = BaseCeilingY;
                                                  GET.pItem->Reserved_36 =0;
                                        }else {
                                                  // crane has not yet reached the ceiling
                                                  // change vertical speed
                                                  // verifying if previously the crane was moving in opposite direction:
                                                  if (GET.pItem->Reserved_36 > 0) {
                                                            // it was moving down, and now up: set a little vertical speed
                                                            GET.pItem->Reserved_36 = -1;
                                                  }else {
                                                            // it was still or moving up: set standard speed
                                                            GET.pItem->Reserved_36 = -16;
                                                  }
                                        }
                              }
                    }
                    // verify if there i command to fall down the crane.
                    // JUMP command but also crane has to be hanged on the ceiling
                    if ((GET.Input.GameCommandsRead & enumCMD.JUMP)!=0 &&
                              GET.pItem->CordY == BaseCeilingY){
                              // command to left fall down the crane while it hanged on the ceiling
                              // but if we are already in this pahse: ignore the command
                              if ((GET.pItem->Reserved_3A & CRANE_FALLING_DOWN)==0) {
                                        // begin slowly and then it will be icnreased fastly
                                        GET.pItem->Reserved_36 = 1;
                                        GET.pItem->Reserved_3A |= CRANE_FALLING_DOWN;
                              }
                    }
                    // if it has been engaged a vertical movement, remove the flag for horizontally moving.
                    if (GET.pItem->Reserved_36 != 0) {

                              GET.pItem->Reserved_3A &= ~CRANE_MOVING_HORIZONTALLY;
                    }
                    // variable to remember it has been chosen a new direction
                    TestChanged=false;
                    // now we check for horizontal moving.
                    // they will work only when ACTION command is not present, and the crane is not movign vertically
                    if ((GET.Input.GameCommandsRead & enumCMD.ACTION) == 0 &&
                              GET.pItem->CordY == BaseCeilingY &&
                              GET.pItem->Reserved_36 == 0) {
                              // there is no ACTION key hitten, crane is hanged on the ceiling and there is no vertical movement in progress
                              if (GET.Input.GameCommandsRead & enumCMD.LEFT) {
                                        // left: moving to west
                                        TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.WEST,SaveCeilingHeight );
                              }
                              // now try other directions only if it has not yet been changed direction
                              if ((GET.Input.GameCommandsRead & enumCMD.RIGHT) != 0 && TestChanged==false) {
                                        // right: movign to east
                                        TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.EAST,SaveCeilingHeight);
                              }
                              if ((GET.Input.GameCommandsRead & enumCMD.UP) != 0 && TestChanged==false) {
                                        // up(forward): moving to north
                                        TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.NORTH,SaveCeilingHeight);
                              }
                              if ((GET.Input.GameCommandsRead & enumCMD.DOWN) != 0 && TestChanged==false) {
                                        //down(backward): moving to south
                                        TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.SOUTH,SaveCeilingHeight);
                              }                              
                    }
                    // if we have not changed direction, check if old direction is good to continue
                    if (TestChanged==false && (GET.pItem->Reserved_3A & CRANE_MOVING_HORIZONTALLY)!=0) {
                              if (IsCraneFreeDirection(ItemIndex, GET.pItem->Reserved_34,SaveCeilingHeight)==false) {
                                        // stop the horizontal movement
                                        // remove the CRANE_MOVING_HORIZONTALLY flag
                                        GET.pItem->Reserved_3A &= ~CRANE_MOVING_HORIZONTALLY;
                              }
                    }
          }
          // handle animations in according with further direction on vertical movments
          if (GET.pItem->Reserved_36 > 0) {
                    // crane is moving down: open the jaws
                    NumAnim = GetRelativeAnimation(GET.pItem);

                    switch (NumAnim) {
                    case 5:
                              // jaws already open: nothing to do
                              break;
                    case 0:
                              // crane has close jaws: set animations to do open them
                              ForceAnimationForItem(GET.pItem, 1, 1);
                              break;
                    case 6:
                              // crane is grabbing: if there is no grabbed item, force to open the jaws
                              if (GET.pItem->Reserved_38 == -1) {
                                        // jaws are empty, no grabbed item: force to open the jaws
                                        ForceAnimationForItem(GET.pItem, 4,1);
                              }
                              break;
                    default:
                              // all other cases: don't change anything while an animation of change jaws is in progress
                              break;
                    }
          }
          if (GET.pItem->Reserved_36 < 0) {
                    // crane is moving up: close the jaws
                    // at least it was not grabbing an item or the jaws are already close
                    NumAnim = GetRelativeAnimation(GET.pItem);
                    switch(NumAnim)
                    {
                    case 0:
                              // crane is already close: ok, nothing to do
                              break;
                    case 5:
                              // crane is open: close the jaws
                              ForceAnimationForItem(GET.pItem, 2, 0);
                              break;
                    case 6:
                              // crane is grabbing: check if it has really caught some item or less


                              if (GET.pItem->Reserved_38 == -1) {
                                        // jaws are empty, no grabbed item: force to open the jaws
                                        ForceAnimationForItem(GET.pItem, 4,0);
                              }
                              // when there is a grabbed item do not change animation
                              break;
                    default:
                              // crane is changing its jaws : do not change anything now to don't break an animation in the middle
                              break;
                    }
          }
          // move the crane following current horizontal
          if (GET.pItem->Reserved_3A & CRANE_MOVING_HORIZONTALLY) {
                    // move the crane horizontally
                    GetIncrements(GET.pItem->Reserved_34, &IncX, &IncZ, 16);
                    GET.pItem->CordX += IncX;
                    GET.pItem->CordZ += IncZ;
                    // update position in the case it changed room
                    UpdateItemRoom(ItemIndex);
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;
                    // if crane has just changed its direction, we peform a different sound
                    if (OldDirection == GET.pItem->Reserved_34) {
                              // it's the same direction: usual moving sound
                              SoundEffect(GetSFX(SFX_CRANE_MOVING, 309), &GET.pItem->CordX, 0);
                    }else {
                              // in this moment crane changed direction: using a metallic sound to signal this steering
                              // 312 BLADES_CLASH_LOUD
                              SoundEffect(312, &GET.pItem->CordX,0);
                    }
          }
          if (GET.pItem->Reserved_36 != 0) {
                    // there is vertical speed: move vertically the crane
                    Crane_MoveCraneVertically(ItemIndex, BaseCeilingY);
          }
}


Testing up/down movements of the Crane



After having updated the dll with last code, we can test the crane also moving up/down it or letting falling down (JUMP command).
It works enough fine, but we see a problem.
When the crane touch the floor, the pole suddenly disappears, but just moving up newly and the pole will be newly drawn.
What is the reason of this bug?
Do you remember when you did that code to avoid that the pole of the crane was visible from room casted over the crane room?
In that case we got that target, setting as invisible the pole mesh when lara was above of crane on Y axis.
That code was own at begin of ControlCrane() function:

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

          Get(enumGET.LARA,0,0);

          if (GET.pLara->CordY < GET.pItem->CordY &&
                    GET.pItem->Reserved_36 == 0) {
                    // lara is above the crane and the crane is hanged on the ceiling: set invisible the first (pole) mesh
                    GET.pItem->MeshVisibilityMask &= ~1;
          }else {
                    // conditions are not true: get visible the first (pole) mesh
                    GET.pItem->MeshVisibilityMask |= 1;
          }

In above code we checked also if vertical speed was null or less, letting visible the pole when the crane was moving up/down.
Now we discover that code was not so smart.
Because when the crane moved down, reaches the floor, the vertical speed will be null and its coordinate is lower than lara, so it becomes invisible the pole.
We should change this code to get invisible the pole, not working on vertical speed but on position of the crane respect to the ceiling: when the crane is hanged on the ceiling (and lara is higher on Y axis) we'll make invisible the pole mesh, but in any other case, we'll let visible the mesh.
So we'll change above code in this way:

void ControlCrane(short ItemIndex)
{
          int BaseCeilingY;

          Get(enumGET.ITEM, ItemIndex,0);


          Get(enumGET.LARA,0,0);

          // discover the heigth of ceiling
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);

          BaseCeilingY= FLOOR.CeilingHeight +1200;

          if (GET.pLara->CordY < GET.pItem->CordY &&
                    GET.pItem->CordY == BaseCeilingY) {
                    // lara is above the crane and the crane is hanged on the ceiling: set invisible the first (pole) mesh
                    GET.pItem->MeshVisibilityMask &= ~1;
          }else {
                    // conditions are not true: get visible the first (pole) mesh
                    GET.pItem->MeshVisibilityMask |= 1;
          }

We used the coordinate of crane respect to the ceiling as condition to change visibility of pole mesh.
Now we try in game this fixing.




Ok, it works.
We can see at left image, that the pole is yet visible in spite the crane is still on the floor and below of lara on Y axis, by other hand, we have to verify also that it worked in other situation: at right image we see the room casted over the (automatic) crane room, and the pole in this case is not visible.


How to recognize and move different items using our Crane

Our target is very complicated. You think that neither Eidos programmers tried to do a moveable was able to interact with any other item. The only one moveable to have this ability is Lara.
We'll try to realize this target dividing the items in different types.
In according with any type there will a different way to handle it.
To help in our tests we added to crane room different items, one for each type:

Marked with red numbers in picture at left we see:
1) Big Pillar (Animating). In spite it has same shape of another static we converted it to animating because only moveables can be moved by crane. The crane it will be able to move this pillar
2) Vase (shatterable static). It will be destroied by crane in some circustance.
3) Grave chest (static). Crane cann't destroy it and neither move it. It will be seen as floor collision stopping the crane movement.
4) Crocodile (creature killable). Crane will injury or killing him, in according with current vertical speed of movement
5) Jeep keys (little pickup). This item is too little to be affected by crane: the crane will ignore fully it.
6) Pushable object. The crane will move it and then we have to verify that, then to have moved it, the pushing/pulling/walkable features of that item were yet working.
7) Pickup. A bit higher than jeep keys but yet too slim to be affefted by crane. It will be ignored, too
8) Jeep (moveable, vehicle). Since it is a movebale, it will be moved by crane, but then we'll have to verify if after moving it there will be problem with its vehicle skills
9) Mummy (unkillable creature). he will be pushed away from crane collisions, or killed, only when the crane felt down with high vertical speed.
10) Rollingball. It will be moved and then we'll see if we are able to engage it, letting falling down on some slope.



The IsCollidingWithSomeItem() function

To detect if the crane is colliding with some moveable or static, we'll use the IsCollidingWithSomeItem() function.
You can find a full description in Function Collection help file.
Here we give only a short description:

bool IsCollidingWithSomeItem(int ItemIndex, DWORD x, int y, DWORD z, int RoomIndex,
                                                                       int MaxDistance, int MinLargerSide, int MinHeight, int Tollerance);

This function allows to discover if some item, with index ItemIndex, when it will be moved in new position x,y,z of room RoomIndex, it will collide with some item: moveable or static.
The function returns "true" when there is a collision and "false" when it is missing, anyway when there is a collision we can see many other data stored in COLLIDE global structure.
Thanks to these extra data we can know if the item is a moveable or a static, and the real size of its collision box.
About input parameter, other that the position (x,y,z and room) where verify the further collision of main item, we can set values to ignore some item, in according with its size or distance from main item.

The code to grab items

Main code, to detect item to grab, will be placed in Crane_MoveCraneVertically() function.
While crane is moving down, we'll use the IsCollidingWithSomeItem() function. When this function detects a collision with some item, we'll use another (new) function to analyse the kind of object, and in according with the type, perform an action to handle it.

The Crane_MoveCraneVertically() function to detect collisions


void Crane_MoveCraneVertically(short ItemIndex, int BaseCeilingY)
{
          short VSpeed;
          DWORD Flags;
          int ObjectTopY;
          int FloorY;
          int NewY;
          int BaseYGrabbed;
          int IncY;
          void *pProcColl;
          int IndexGrabbed;
          StrItemTr4 *pCrane;
          bool TestCollision;

          VSpeed = GET.pItem->Reserved_36;
          IndexGrabbed= GET.pItem->Reserved_38;

          // ------ Change vertical speed ----------------
          if (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN) {
                    // the crane should falling down
                    // we have to do gravity simulation: increase the Vertical speed until it reaches a max value
                    VSpeed += 8;
                    if (VSpeed > 128) VSpeed=128;
          }else {
                    // the crane should move up/down slowly with constant speed but if the speed is less than default +16/-16
                    // it means we have to simulate a gradual acceleration
                    if (VSpeed != 16 && VSpeed != -16) {
                              // increase or decrease following current sign
                              if (VSpeed < 0) {
                                        //negative speed: decrease
                                        VSpeed--;
                              }else {
                                        // positive: increase
                                        VSpeed++;
                              }
                    }
          }
          // compute future Y coordinate:

          NewY= GET.pItem->CordY + VSpeed;
          // check if crane reached some obstacle:
          if (VSpeed < 0) {
                    // crane was moving up: check if it reached ceiling
                    if (NewY <= BaseCeilingY) {
                              // yes: reached ceiling: stop the movement
                              VSpeed=0;
                              NewY = BaseCeilingY;
                    }
          }else {
                    // crane was moving down

                    FloorY= FLOOR.FloorHeight;

                    // verify if crane is colliding with some item
                    // note: if the crane is grabbing an item, we'll use the grabbed item to detect collisions
                    // since it is below the crane
                    if (IndexGrabbed != -1 ) {
                              // save crane structure:
                              pCrane=GET.pItem;
                              Get(enumGET.ITEM, IndexGrabbed, 0);
                              // to avoid that it will be detected the collision between grabbed item and the crane
                              // we should remove temporarily the collisions of Crane
                              // setting NULL its CollisionProcedure in its Slot structure
                              // get slot structure of the crane
                              Get(enumGET.SLOT, pCrane->SlotID,0);
                              // save previous value for collisione procedure:
                              pProcColl = GET.pSlot->pProcCollision;
                              // set null
                              GET.pSlot->pProcCollision = NULL;
                              // compute the new coordinate for grabbed item
                              IncY = NewY - pCrane->CordY;          
                              TestCollision = IsCollidingWithSomeItem(IndexGrabbed,
                                                            GET.pItem->CordX, GET.pItem->CordY+IncY, GET.pItem->CordZ, GET.pItem->Room, 3072, 0, 256,-64);
                              // check also collision between grabbed item and floor when it's missing a collision with items
                              if (TestCollision==false) {
                                        // discover lower Y coordinate of grabbed item
                                        Get(enumGET.ITEM_COLL_BOX, IndexGrabbed, 0);
                                        BaseYGrabbed = GET.pItem->CordY + GET.pCollItem->MaxY;

                                        if (BaseYGrabbed >= FloorY) {
                                                  // grabbed item touches the floor: stop like a collision
                                                  TestCollision=true;
                                        }
                              }
                              // restore all data
                              GET.pSlot->pProcCollision = pProcColl;
                              GET.pItem = pCrane;
                              
                              // if there is collision: stop here
                              if (TestCollision) {
                                        // restore old Y coordinate of crane
                                        NewY= GET.pItem->CordY;
                                        // and clear vertical speed
                                        VSpeed=0;
                              }
                    }else {
                              // no grabbed item: check collision between crane and other objects:
                              Flags=AFO_IGNORE;
                              if (IsCollidingWithSomeItem(ItemIndex, GET.pItem->CordX, NewY, GET.pItem->CordZ, GET.pItem->Room,
                                        3072, 250, 256, -128)==true) {
                                        // found a collision: analyse what to do in according with object type
                                         Flags = ActionForObject(ItemIndex, &ObjectTopY, VSpeed);
                                         // restore GET.pItem because ActionForObject() function could have changed it
                                         Get(enumGET.ITEM, ItemIndex,0);
                              }
                              switch (Flags) {
                              case AFO_STOP_ON_COLLISION:
                                        // use as floory the top Y coordinate of object
                                        FloorY = ObjectTopY;
                                        break;
                              case AFO_GRAB_LARGE:
                              case AFO_GRAB_SLIM:
                                        // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                        GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                        IndexGrabbed= COLLIDE.ItemIndex;
                                        SignalMovedItem(IndexGrabbed);
                                        // set animation of grabbing
                                        if (Flags == AFO_GRAB_LARGE) {
                                                  // from open to large grabbing
                                                  ForceAnimationForItem(GET.pItem, 3, 2);
                                        }
                                        if (Flags == AFO_GRAB_SLIM) {
                                                  // from open to slim grabbing

                                                  ForceAnimationForItem(GET.pItem, 12, 3);
                                        }
                                        // and stop speed
                                        VSpeed=0;
                                        break;
                              }
                              // check if crane reached the floor
                              if (NewY >= FloorY) {
                                        // yes: reached the floor: stop the movement
                                        VSpeed=0;
                                        NewY = FloorY;

                                        // effects to have touched the floor

                                        ForceAnimationForItem(GET.pItem, 2, 0);
                                        SoundEffect(GetSFX(SFX_CRANE_TOUCH_FLOOR, 314), &GET.pItem->CordX, 0);
                                        if (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN) {
                                                  // to do earthquake only if there was falling down
                                                  // earth quake: flipeffect 1
                                                  PerformFlipeffect(NULL, 1, 0, 0);

                                                  // remove the CRANE_FALLING_DOWN flag
                                                  GET.pItem->Reserved_3A &= ~CRANE_FALLING_DOWN;
                                        }                                        
                              }
                    }
          }
          // update values:                    
          GET.pItem->Reserved_36 = VSpeed;
          // compute the differnce in Y coordinate of crane and apply same change for grabbed item
          IncY = NewY - GET.pItem->CordY;
          if (IncY != 0) {
                    GET.pItem->CordY = NewY;
                    // check for change of room
                    UpdateItemRoom(ItemIndex);
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;                    

                    // if there is a grabbed item change also its coordinate Y
                    IndexGrabbed=GET.pItem->Reserved_38;

                    if (IndexGrabbed != -1) {
                              // save crane structure
                              pCrane= GET.pItem;

                              // get structure of grabbed item
                              Get(enumGET.ITEM, IndexGrabbed, 0);
                              GET.pItem->CordY += IncY;
                              UpdateItemRoom(IndexGrabbed);
                              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                               GET.pItem->HeightFloor = FLOOR.FloorHeight;
                              // restore crane structure
                              GET.pItem = pCrane;
                    }
          }
}

Most changes are in moving down direction, of course.
When the crane is moving down, we have, as first point, to verify if crane is already grabbing an item, or it is empty.
Since, when the crane is grabbing an item, it will be the collision box of this item (placed below the crane) to be the first side of impact, so we'll verify if that grabbed item is touching something moving down:

                    if (IndexGrabbed != -1 ) {
                              // save crane structure:
                              pCrane=GET.pItem;
                              Get(enumGET.ITEM, IndexGrabbed, 0);
                              // to avoid that it will be detected the collision between grabbed item and the crane
                              // we should remove temporarily the collisions of Crane
                              // setting NULL its CollisionProcedure in its Slot structure
                              // get slot structure of the crane
                              Get(enumGET.SLOT, pCrane->SlotID,0);
                              // save previous value for collisione procedure:
                              pProcColl = GET.pSlot->pProcCollision;
                              // set null
                              GET.pSlot->pProcCollision = NULL;
                              // compute the new coordinate for grabbed item
                              IncY = NewY - pCrane->CordY;
          
                              TestCollision = IsCollidingWithSomeItem(IndexGrabbed,
                                                            GET.pItem->CordX, GET.pItem->CordY+IncY, GET.pItem->CordZ, GET.pItem->Room, 3072, 0, 256,-64);
                              // check also collision between grabbed item and floor when it's missing a collision with items
                              if (TestCollision==false) {
                                        // discover lower Y coordinate of grabbed item
                                        Get(enumGET.ITEM_COLL_BOX, IndexGrabbed, 0);
                                        BaseYGrabbed = GET.pItem->CordY + GET.pCollItem->MaxY;

                                        if (BaseYGrabbed >= FloorY) {

                                                  // grabbed item touches the floor: stop like a collision
                                                  TestCollision=true;
                                        }
                              }
                              // restore all data
                              GET.pSlot->pProcCollision = pProcColl;
                              GET.pItem = pCrane;
                              // if there is collision: stop here
                              if (TestCollision) {
                                        // restore old Y coordinate of crane
                                        NewY= GET.pItem->CordY;
                                        // and clear vertical speed
                                        VSpeed=0;
                              }

In above code, we see a little trick to use in some circustance before calling the IsCollidingWithSomeItem() function:
Since the crane is grabbing an item, when we check if this grabbed item is colliding with some other item, it's sure that there will be a collision between the crane and the grabbed item, but we don't wish verify this situation but only collision with other items, different than crane.
So we remove temporarily the collision procedure of the crane to get it was ingored by colliding function:

                              // get slot structure of the crane
                              Get(enumGET.SLOT, pCrane->SlotID,0);
                              // save previous value for collisione procedure:
                              pProcColl = GET.pSlot->pProcCollision;
                              // set null
                              GET.pSlot->pProcCollision = NULL;

Then we called the IsCollidingWithSomeItem() function, we should remember to restore the collision procedure, of course:

                              GET.pSlot->pProcCollision = pProcColl;



When the crane is empty....
But now we see the code when the crane is empty, to verify if we can grab an item:

else {
                              // no grabbed item: check collision between crane and other objects:
                              Flags=AFO_IGNORE;
                              if (IsCollidingWithSomeItem(ItemIndex, GET.pItem->CordX, NewY, GET.pItem->CordZ, GET.pItem->Room,
                                        3072, 250, 256, -128)==true) {
                                        // found a collision: analyse what to do in according with object type
                                         Flags = ActionForObject(ItemIndex, &ObjectTopY, VSpeed);
                                         // restore GET.pItem because ActionForObject() function could have changed it
                                         Get(enumGET.ITEM, ItemIndex,0);
                              }
                              switch (Flags) {
                              case AFO_STOP_ON_COLLISION:
                                        // use as floory the top Y coordinate of object
                                        FloorY = ObjectTopY;
                                        break;
                              case AFO_GRAB_LARGE:
                              case AFO_GRAB_SLIM:
                                        // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                        GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                        IndexGrabbed= COLLIDE.ItemIndex;
                                        SignalMovedItem(IndexGrabbed);
                                        // set animation of grabbing
                                        if (Flags == AFO_GRAB_LARGE) {
                                                  // from open to large grabbing
                                                  ForceAnimationForItem(GET.pItem, 3, 2);
                                        }
                                        if (Flags == AFO_GRAB_SLIM) {
                                                  // from open to slim grabbing
                                                  ForceAnimationForItem(GET.pItem, 12, 3);
                                        }
                                        // and stop speed
                                        VSpeed=0;
                                        break;
                              }

In above code we see a new trng funtion:

                                        SignalMovedItem(IndexGrabbed);

It's necessary to be sure that moveable had its coordinate (and new position we are changing) saved in savegame.
It happens that not all moveable save their position in savegame. The creature did it but, animating did not it.
Since when we moved an item in a different position we wish that its new position was preserved in savegame/reload savegames, we use SignalMovedItem() function to inform trng that we want that the position of this item was saved in savegame.

About the parameters used for colliding function we see that we set many filters for objects to detect:

                              if (IsCollidingWithSomeItem(ItemIndex, GET.pItem->CordX, NewY, GET.pItem->CordZ, GET.pItem->Room,
                                        3072, 250, 256, -128)==true) {

For instance we wish ignore little items "250" as MinLargerSide, and "256" as MinHeight.
Interesting is the negative value for Tollerance (-128). It seems a bit heigh as absolute value, since it will reduce the size of other collision boxes of 128 game units for side and one click for whole size. The reason is to avoid a situation like this:


Since the movement is vertical, if we let unchanged the original collision boxes, it could happen that we see in above left picture: the crane it will be stopped only because a border of collision box of the jeep is touching it.
While we prefer ignoring that little touch, since it should be innatural seeing the crane floating in the empty only for that little collision.
In right picture you see what happens with heigh negative tollerance: the collision doesn't happen and the crane is able to reach the floor.
Please, note that this trick is suggested only when main item is moving vertically, because, in the case the item was moving horizontally, there should be no reason to reduce the collision box.
When it will be detected a collision we'll begin a long computation to discover what kind of object is it and take some action to do.

                                        // found a collision: analyse what to do in according with object type
                                         Flags = ActionForObject(ItemIndex, &ObjectTopY, VSpeed);

Since the code to manage this situation is very long we put it in a new function: the ActionForObject() function.

The ActionForObject() function

This function will take the data from COLLIDE global structure where the IsCollidingWithSomeItem() function wrote the collision infos.
Once it has discovered the object type, it will be performed some action and then it will be returned some flags (of AFO_ type: Action For Object) to inform the calling code about what it has been performed, and then it will write also in ObjectTopY local variable, the coordinate of top side of object where the crane collided. This value will be used when the object stopped the crane.

DWORD ActionForObject(short ItemIndex, int *pCollisionYCoord, int VerticalSpeed)
{
          int SizeX;
          int SizeZ;
          StrBoxCollisione *pColl;
          bool TestStatic;
          StrPosizione Pos;
          StrItemTr4 *pCrane;
          bool TestFast;
          int Distance;
          int MinSize;
          int MaxSize;
          
          // save the crane structure in pCrane because we need of GET.pItem for further moveable with whom there is the collision
          pCrane= GET.pItem;
          
          // item could be a moveable or a static: try to get them data in common variables
          if (COLLIDE.ItemIndex != -1) {
                    // it is a moveable
                    Get(enumGET.ITEM_COLL_BOX, COLLIDE.ItemIndex,0);
                    pColl= GET.pCollItem;
                    // get position of item
                    Get(enumGET.ITEM, COLLIDE.ItemIndex, 0);
                    Pos.OrgX = GET.pItem->CordX;
                    Pos.OrgY = GET.pItem->CordY;
                    Pos.OrgZ = GET.pItem->CordZ;

                    TestStatic=false;
          }else {
                    // it is a static
                    Get(enumGET.STATIC_COLL_BOX, COLLIDE.StaticIndexRoom, COLLIDE.StaticIndex);
                    pColl= GET.pCollStatic;
                    // get position of static
                    Get(enumGET.STATIC, COLLIDE.StaticIndexRoom , COLLIDE.StaticIndex );
                    Pos.OrgX = GET.pStatic->x;
                    Pos.OrgY = GET.pStatic->y;
                    Pos.OrgZ = GET.pStatic->z;
                    TestStatic=true;
          }
          // set the Y coordinate of top side
          *pCollisionYCoord = Pos.OrgY + pColl->MinY;

          // discover size of both sides
          SizeX= pColl->MaxX - pColl->MinX;
          SizeZ= pColl->MaxZ - pColl->MinZ;
          // the IsCollideWithSomeItem() function, had already excluded item too little, but now we
          // have to exclude from grabbing also the items too slim in their littler side

          // discover size of shorter side
          MinSize= SizeX;
          if (SizeZ < MinSize) MinSize = SizeZ;

          // discover length of larger side
          MaxSize= SizeX;
          if (SizeZ > MaxSize) MaxSize=SizeZ;

          // compute the distance between two origins
          Distance=GetDistanceXZ(pCrane->CordX, pCrane->CordZ, Pos.OrgX, Pos.OrgZ);
          
          // discover if crane felt down: in this case there is an high vertical speed
          if (pCrane->Reserved_3A & CRANE_FALLING_DOWN) {
                    TestFast=true;
          }else {
                    TestFast=false;
          }
          // now we have many data: discover the action to perform

          // ---------- ANALYSE FOR STATICS --------------------------
          if (TestStatic==true) {
                    // discover if static is a shatter
                    if (GET.pStatic->SlotId >= Trng.pGlobTomb4->pBaseCustomize->ShatterInizio &&
                              GET.pStatic->SlotId <= Trng.pGlobTomb4->pBaseCustomize->ShatterFine) {
                              
                              // it's a shatter: destroy it but only if there is falling down of crane
                              if (TestFast==true) {
                                        DestroyShatterObject(COLLIDE.StaticIndex, COLLIDE.StaticIndexRoom);
                                        return AFO_SHATTER_OBJECT;
                              }
                    }
                    // it's not possible destroying it.
                    // if its position is very closed to crane position (center pivot for both) and
                    // the minsize of static is very little, we can ignore it
                    if (MinSize < 256 && Distance < 128) {
                              // ignore it!
                              return AFO_IGNORE;
                    }

                    // in other situation: we consider it like a floor collision to stop the crane before touching the floor
                    return AFO_STOP_ON_COLLISION;
          }
          // ---------- ANALYSE FOR MOVEABLES -------------------------
          // discover if the moveable is a creature
          Get(enumGET.INFO_ITEM, COLLIDE.ItemIndex,0);
          if (GET.InfoItem.TestCreature == true) {
                    // the crane will injury or killing creature only if there is
                    // a verticalspeed or there is animation 2 (it is "biting")
                    if (VerticalSpeed != 0 || GetRelativeAnimation(pCrane) == 2) {

                              // yes it is a creature
                              if (GET.InfoItem.TestSemiGod == true || GET.InfoItem.TestOnlyExplode == true) {
                                        // it is a semi-god or requires explosive ammo
                                        // if there is fast speed kill him anyway
                                        if (TestFast==true) {
                                                  KillSemigod(COLLIDE.ItemIndex);
                                                  return AFO_KILL_CREATURE;
                                        }
                                        // not fast: push away it
                                        ItemPushAwayItem(ItemIndex, COLLIDE.ItemIndex);
                                        return AFO_PUSH_AWAY_CREATURE;
                              }
                              // it's a semigod:
                              // if crane is falling down: kill immedialty
                              if (TestFast==true) {
                                        GET.pItem->Health=0;
                                        return AFO_KILL_CREATURE;
                              }
                              if (GET.InfoItem.TestOnlyExplode == false) {
                                        GET.pItem->Health -= 10;
                              }
                    }
                    // crane is moving slowly push aways the creature
                    ItemPushAwayItem(ItemIndex, COLLIDE.ItemIndex);
                    return AFO_INJURY_CREATURE;
          }
          // it's not a creature:
          // discover if we can ignore it
          // if its position is very closed to crane position (pivots, at center, aligned) and
          // the minsize of static is very little, we can ignore it
          if (MinSize < 256 && Distance < 128) {
                    // ignore it!
                    return AFO_IGNORE;
                    }
          // verify if we can grabbing it
          if (TestFast==false && VerticalSpeed != 0) {
                    // crane was moving down slowly: ok
                    // crane and item has to be very closed as pivots to avoid to grab only some its border
                    // with exception if the item has a very large collision box (like jeep)
                    if (Distance < 256 || MaxSize > 1200) {
                              // ok: grabbing: to see if it's better using the large grabbing or that slim
                              if (MaxSize > 700)           return AFO_GRAB_LARGE;
                              return AFO_GRAB_SLIM;
                    }
          }
          // last chance: consider it as collision
          return AFO_STOP_ON_COLLISION;
}

In first rows the function raises many infos about the detected object but also about crane, for instance if it was falling down or less:

          // discover if crane felt down: in this case there is an high vertical speed
          if (pCrane->Reserved_3A & CRANE_FALLING_DOWN) {
                    TestFast=true;
          }else {
                    TestFast=false;
          }

Because there will be different action in according with the speed: if the crane moved up for falling down, it will be possible smash the shatter objects, but it will be not possible grabbing items.
We stored this info in local variable TestFast.

When the object is a static, the code will try to discover if it is a shatter or a static so slim to be ignored, but only when it is at center of sector, where the crash has always a space:

                    // if its position is very closed to crane position (center pivot for both) and
                    // the minsize of static is very little, we can ignore it
                    if (MinSize < 256 && Distance < 128) {
                              // ignore it!
                              return AFO_IGNORE;
                    }

Distance is the distance between crane and object only in horizontal plane. Since the crane is always at center of the sector, if the distance is very litte this means that the object is at center of the sector and when its size is short, it will be ignored.


When the object is a moveable, the code discover what kind of moveable with the function:

          Get(enumGET.INFO_ITEM, COLLIDE.ItemIndex,0);

If the item is a creature: it will be injured, killed or pushed away, using the other new function: ItemPushAwayItem() function.

The ItemPushAwayItem() function

This function works like ItemPushLara() function, but since that tomb raider function works only with lara, we tried to build another function with same target.
This function will move aways from first item, the second item, to put it outside of collision box of first item:

void ItemPushAwayItem(short ItemMainIndex, short SecondaryIndex)
{
          StrItemTr4 *pMain;
          short Direction;
          int IncX;
          int IncZ;
          StrItemTr4 *pSaveItem;
          int Radius;
          int TopY;

          pSaveItem= GET.pItem;

          // discover radius (medium half size of main item):
          GetItemSize(ItemMainIndex, &TopY, &Radius);

          Get(enumGET.ITEM, ItemMainIndex,0);
          pMain= GET.pItem;

          Get(enumGET.ITEM, SecondaryIndex,0);
          // discover direction from main item to secondary item:
          Direction=GetDirection(pMain->CordX, pMain->CordZ, GET.pItem->CordX, GET.pItem->CordZ);
          GetIncrements(Direction, &IncX, &IncZ, 32);
          // to push away the item until there are no more collision between two collision boxes
          while (TestBoundCollide(pMain, GET.pItem, Radius) == true) {
                    // move secondary item
                    GET.pItem->CordX += IncX;
                    GET.pItem->CordZ += IncZ;
          }
          UpdateItemRoom(SecondaryIndex);
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          GET.pItem->HeightFloor = FLOOR.FloorHeight;
          // restore GET.pItem
          GET.pItem = pSaveItem;
}

This function is not so smart, anyway it's enough to avoid that some semi-god item (not killable) was "wrapped" by crane with no effect.

Finally, when the crane is moving slowly and the object is a moveable but not a creature, we'll verify if we can grab it, and also the size of grabbing in according with size of item:

          if (TestFast==false && VerticalSpeed != 0) {
                    // crane was moving down slowly: ok
                    // crane and item has to be very closed as pivots to avoid to grab only some its border
                    // with exception if the item has a very large collision box (like jeep)
                    if (Distance < 256 || MaxSize > 1200) {
                              // ok: grabbing: to see if it's better using the large grabbing or that slim
                              if (MaxSize > 700)           return AFO_GRAB_LARGE;
                              return AFO_GRAB_SLIM;
                    }
          }

With above code (to verify) we set that, when the item has as max size 700 game units or greater, we'll use large grabbing, while,below 700, we'll use slimb grabbing animations.


Moving the grabbed item

Coming back a moment to Crane_MoveCraneVertically() function, we see that we have to move also the grabbed item, following same movements of the crane:

          // update values:                    
          GET.pItem->Reserved_36 = VSpeed;
          // compute the differnce in Y coordinate of crane and apply same change for grabbed item
          IncY = NewY - GET.pItem->CordY;
          if (IncY != 0) {
                    // check for change of room
                    GET.pItem->CordY = NewY;
                    UpdateItemRoom(ItemIndex);
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;
                    // if there is a grabbed item change also its coordinate Y
                    IndexGrabbed=GET.pItem->Reserved_38;
                    if (IndexGrabbed != -1) {
                              // save crane structure
                              pCrane= GET.pItem;
                              // get structure of grabbed item
                              Get(enumGET.ITEM, IndexGrabbed, 0);
                              GET.pItem->CordY += IncY;
                              UpdateItemRoom(IndexGrabbed);
                              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                              GET.pItem->HeightFloor = FLOOR.FloorHeight;
                              // restore crane structure
                              GET.pItem = pCrane;
                    }
          }

We used the trick to compute the difference in Y coordinate about Crane we performed in this frame inside of Crane_MoveCraneVertically() function:

          // compute the differnce in Y coordinate of crane and apply same change for grabbed item
          IncY = NewY - GET.pItem->CordY;

And then we applied same "IncY' difference, on Y coordinate of the grabbed item, to do move it togheter with the crane:

                              // get structure of grabbed item
                              Get(enumGET.ITEM, IndexGrabbed, 0);
                              GET.pItem->CordY += IncY;
                              UpdateItemRoom(IndexGrabbed);

                              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                              GET.pItem->HeightFloor = FLOOR.FloorHeight;

We have to do the same also for horizontal movements, of course, but in this case, we'll type the code in ControlCraneManual() function

The changes in ControlCraneManual() function to grab items

Also in ControlCraneManual() function we did some change, of course.
We need to handle the command to release the grabbed item, but also we have to update the coordinates of grabbed items until it is travelling togheter with the crane.
Another important change about ControlCraneManual() function, is its reorganization, because it became too long and messed.
So we divided the code of this main function, in many little units, sub-functions, with specific targets, like we did also with Star Wars Robot.
The ControlCraneManual() function now has this code:

void ControlCraneManual(short ItemIndex)
{
          int BaseCeilingY;
          short OldDirection;

          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle != ItemIndex) {
                    // current vehicle is NOT the current crane: quit
                    return;
          }
          // current crane it has been engaged...
          AnimateItem(GET.pLara);
          Get(enumGET.ITEM,ItemIndex,0);
          // ------------- WAIT FOR END OF ANIMATION TO ENGAGE CRANE -------------------
          if (Crane_WaitEndEngageAnimation(ItemIndex)==false) return;
          // -------------- GET SOME GLOBAL DATA ---------------------------
          Get(enumGET.INPUT,0,0);
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          // save Y position where it should be crane to be hanged on the ceiling:
          BaseCeilingY = FLOOR.CeilingHeight + 1200;
          // saved current (old) direction to discover if after game input it has been changed:
          OldDirection= GET.pItem->Reserved_34;
          // discover if the crane is at center of current sector.
          // only in this situation we could accept many game commands to change direction, open/close jaws, move up/down ect.
          if (FLOOR.SectorCoords.Radius < 15 &&
                    (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN)==0) {
                    // -------- INPUT COMMANDS TO MOVE VERTICALLY THE CRANE --------------------------
                    Crane_InputVerticalMov(ItemIndex, GET.Input.GameCommandsRead, BaseCeilingY);
                    // -------- INPUT COMMANDS TO MOVE HORIZONTALLY THE CRANE ------------------------
                    Crane_InputHorizontalMov(ItemIndex, GET.Input.GameCommandsRead, BaseCeilingY);
                    // --------- INPUT COMMANDS: ALL OTHERS ----------------------------------------------
                    if (Crane_InputOthers(ItemIndex, GET.Input.GameCommandsRead)==true) {
                              // required to quit the driving of the crane
                              return;
                    }
          }
          // -------------- ANIMATION MANAGEMENT ----------------------------
          Crane_UpdateAnimations(ItemIndex);
          // -------------- MOVE CRANE HORIZONTALLY --------------------------
          if (GET.pItem->Reserved_3A & CRANE_MOVING_HORIZONTALLY) {

                    Crane_MoveCraneHorizontally(ItemIndex, OldDirection);
          }
          // -------------- MOVE CRANE VERTICALLY -------------------------
          if (GET.pItem->Reserved_36 != 0) {
                    // there is vertical speed: move vertically the crane
                    Crane_MoveCraneVertically(ItemIndex, BaseCeilingY);
          }
}

Now we see all these new sub-function and their news

The Crane_WaitEndEngageAnimation() function

In this function it will be checked if first animation to engage the crane has been completed.
When it has been completed the function it will return "true"

bool Crane_WaitEndEngageAnimation(short ItemIndex)
{
          int IndexNgle;

          if ((GET.pItem->Reserved_3A & CRANE_START_DRIVING)==0) {
                    // but the driving is not yet begun
                    // verify if the first hardcoded animation it has been completed
                    if (GET.pLara->AnimationNow != 445) {
                              // animation of lara is different: it has been completed, now we begin the driving
                              GET.pItem->Reserved_3A |= CRANE_START_DRIVING;
                              // and only in this frame we send the GT_DRIVING_CRANE_START event
                              // convert the index of the crane in ngle format:
                              IndexNgle= FromTomb4IndexToNgleIndex(ItemIndex);
                              // send the event
                              DetectedGlobalTriggerEvent(GT_DRIVING_CRANE_START, IndexNgle, false);
                    }
          }
          // if the driving it's not yet engaged we quit the code:
          if ((GET.pItem->Reserved_3A & CRANE_START_DRIVING)==0) {
                    // we are yet waiting that the first animation of lara was completed...
                    return false;
          }
          return true;
}


The Crane_InputVerticalMov() function

Detect if there are input commands to do move the crane up / down but also to let falling down the crane

// receive the flags for game commands and BaseCeilingY (= Y coordinate when crane is hanged on the ceiling)
void Crane_InputVerticalMov(short ItemIndex, DWORD FlagsInput, int BaseCeilingY)
{
          int IndexGrabbed;

          IndexGrabbed=GET.pItem->Reserved_38;
          // since when there is ACTION(ctrl) down the direction will work only to move up/down the crane
          // we analyse the two situations: when the CTRL key is down, and when is not
          if (FlagsInput & enumCMD.ACTION) {
                    // ACTION is down: only up/down like directions, to move up/down the crane
                    if (FlagsInput & enumCMD.DOWN) {
                              // ---------- MOVE DOWN SLOWLY THE CRANE -----------------
                              // set a vertical speed to do move down the crane. set a positive value.
                              // if previous vertical speed was 0 or yet positive, we set immediatly the standard vspeed
                              // while if it was negative (the crane was moving up a moment ago) to simulate the inversion of
                              // direction, we place a very little speed and we'll increase it until reach standard speed in
                              // next frame
                              if (GET.pItem->Reserved_36 >= 0) {
                                        // standard speed to move down (slowly)
                                        GET.pItem->Reserved_36 = 16;
                              }else {
                                        // the crane was moving up, and now there is the command to invert direction
                                        // we begin with very little speed
                                        GET.pItem->Reserved_36 = 1;
                              }
                    }
                    if (FlagsInput & enumCMD.UP) {
                              // -------- MOVE UP SLOWLY THE CRANE --------------------
                              // if the crane already reached the ceiling we need to ignore this command
                              // and force the baseceiling y
                              if (GET.pItem->CordY <= BaseCeilingY) {
                                        // crane reached ceiling: stop here
                                        GET.pItem->CordY = BaseCeilingY;
                                        GET.pItem->Reserved_36 =0;
                              }else {
                                        // crane has not yet reached the ceiling
                                        // change vertical speed
                                        // verifying if previously the crane was moving in opposite direction:
                                        if (GET.pItem->Reserved_36 > 0) {
                                                  // it was moving down, and now up: set a little vertical speed
                                                  GET.pItem->Reserved_36 = -1;
                                        }else {
                                                  // it was still or moving up: set standard speed
                                                  GET.pItem->Reserved_36 = -16;
                                        }
                              }
                    }
          }
          // verify if there i command to fall down the crane.
          // JUMP command but also crane has to be hanged on the ceiling and no grabbed item
          if ((FlagsInput & enumCMD.JUMP)!=0 &&
                    IndexGrabbed == -1 &&
                    GET.pItem->CordY == BaseCeilingY){
                    // command to left fall down the crane while it hanged on the ceiling

                    // begin slowly and then it will be icnreased fastly
                    GET.pItem->Reserved_36 = 1;
                    GET.pItem->Reserved_3A |= CRANE_FALLING_DOWN;          
          }
          // if it has been engaged a vertical movement, remove the flag for horizontally moving.
          if (GET.pItem->Reserved_36 != 0) {

                    GET.pItem->Reserved_3A &= ~CRANE_MOVING_HORIZONTALLY;
          }
}

The input parameter "DWORD FlagsInput", it will be the value in "GET.Input.GameCommandsRead" variable, in this way, inside of this function, we can check the input commands using a shorter variable but with same values.

Respect to previous version, you can see that now the falling down of the crane it has been forbidden when the crane is grabbing an item:

          // verify if there i command to fall down the crane.
          // JUMP command but also crane has to be hanged on the ceiling and no grabbed item
          if ((FlagsInput & enumCMD.JUMP)!=0 &&
                    IndexGrabbed == -1 &&


The Crane_InputHorizontalMov() function

This function check for commands: LEFT, RIGHT, FORWARD and BACKWARD when the ACTION key is not present.
The code is pratically the same of previous version

void Crane_InputHorizontalMov(short ItemIndex, DWORD InputFlags, int BaseCeilingY)
{
          bool TestChanged;
          int SaveCeilingHeight;

          // save ceiling height, since IsCraneFeeeDirection() will change FLOOR data
          SaveCeilingHeight = FLOOR.CeilingHeight;

          // variable to remember it has been chosen a new direction
          TestChanged=false;
          // now we check for horizontal moving.
          // they will work only when ACTION command is not present, and the crane is not movign vertically
          if ((InputFlags & enumCMD.ACTION) == 0 &&
                    GET.pItem->CordY == BaseCeilingY &&
                    GET.pItem->Reserved_36 == 0) {

                    // there is no ACTION key hitten, crane is hanged on the ceiling and there is no vertical movement in progress
                    if (InputFlags & enumCMD.LEFT) {
                              // left: moving to west
                              TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.WEST,SaveCeilingHeight);
                    }
                    // now try other directions only if it has not yet been changed direction
                    if ((InputFlags & enumCMD.RIGHT) != 0 && TestChanged==false) {
                              // right: movign to east
                              TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.EAST,SaveCeilingHeight);
                    }
                    if ((InputFlags & enumCMD.UP) != 0 && TestChanged==false) {
                              // up(forward): moving to north
                              TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.NORTH,SaveCeilingHeight);
                    }
                    if ((InputFlags & enumCMD.DOWN) != 0 && TestChanged==false) {
                              //down(backward): moving to south
                              TestChanged= IsCraneFreeDirection(ItemIndex, enumORIENT.SOUTH,SaveCeilingHeight);
                    }
          }
          // if we have not changed direction, check if old direction is good to continue
          if (TestChanged==false && (GET.pItem->Reserved_3A & CRANE_MOVING_HORIZONTALLY)!=0) {
                    if (IsCraneFreeDirection(ItemIndex, GET.pItem->Reserved_34,SaveCeilingHeight)==false) {
                              // stop the horizontal movement
                              // remove the CRANE_MOVING_HORIZONTALLY flag
                              GET.pItem->Reserved_3A &= ~CRANE_MOVING_HORIZONTALLY;
                    }
          }
}


The Crane_InputOthers() function

This function handles the input commands differents by moving commands for horizontal or vertical movements.
For instance it checks for ROLL(end) command that quits the driving of the crane and when it detects this command the function will return "true".

// management of game input commands different than movements
// note: if there is the command to quit the driving, it will return "true", otherwise "false"
bool Crane_InputOthers(short ItemIndex, DWORD FlagsInput)
{
          int IndexGrabbed;
          int NumAnim;
          StrItemTr4 *pCrane;

          IndexGrabbed=GET.pItem->Reserved_38;

          // Crane is at center of current sector.and it is not falling down
          // now we can perform some game commands
          if (FlagsInput & enumCMD.ROLL) {
                    // command ROLL(end): quit driving crane:
                    Crane_QuitDriving(ItemIndex);
                    return true;
          }
          // check for "Open Jaws" command:
          if (FlagsInput & enumCMD.DRAW_WEAPON) {
                    // there is command to open jaws
                    // Change animation only if jaws was in grabbing position
                    NumAnim = GetRelativeAnimation(GET.pItem);

                    switch (NumAnim) {
                    case 6:
                              // crane was with large grabbing
                              // set animation to open large grabbin jaws
                              ForceAnimationForItem(GET.pItem, 4, 1);
                              break;
                    case 8:
                              // crane had slim grabbing jaws
                              // set animation to open slimb grabbing jaws
                              ForceAnimationForItem(GET.pItem, 9, 1);
                              break;
                    }
                    // if crane was grabbing an item: manage the falling down of the grabbed item
                    if (IndexGrabbed != -1) {
                              // place the item on the floor (temporary solution: then we add a better code)
                              // save the crane structure
                              pCrane=GET.pItem;
                              Get(enumGET.ITEM, IndexGrabbed,0);
                              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                              // discover the Y bottom side of grabbed item
                              // (for some items, like the rollingball) it could be different than 0
                              Get(enumGET.ITEM_COLL_BOX, IndexGrabbed,0);
                              // set y coordinate of grabbed item with floor value
                              GET.pItem->CordY = FLOOR.FloorHeight - GET.pCollItem->MaxY;
                              GET.pItem->HeightFloor = FLOOR.FloorHeight;
                              UpdateItemRoom(IndexGrabbed);
                              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                              GET.pItem->HeightFloor = FLOOR.FloorHeight;
                              // restore crane structure
                              GET.pItem = pCrane;
                              // remove data for grabbed item
                              IndexGrabbed=-1;
                              GET.pItem->Reserved_38 = -1;
                    }          
          }
          return false;
}

In this function the great news it own the management of DRAW WEAPONS(space) to release the grabbed item.
As you can see, there will be also the setting of new animation to do open the jaws, choosing right animation in according with current grabbing:

          // check for "Open Jaws" command:
          if (FlagsInput & enumCMD.DRAW_WEAPON) {
                    // there is command to open jaws
                    // Change animation only if jaws was in grabbing position
                    NumAnim = GetRelativeAnimation(GET.pItem);

                    switch (NumAnim) {
                    case 6:
                              // crane was with large grabbing
                              // set animation to open large grabbin jaws
                              ForceAnimationForItem(GET.pItem, 4, 1);
                              break;
                    case 8:
                              // crane had slim grabbing jaws
                              // set animation to open slimb grabbing jaws
                              ForceAnimationForItem(GET.pItem, 9, 1);
                              break;
                    }

Then we check if there was really a grabbed item and in that case we place it on the floor:

                    // if crane was grabbing an item: manage the falling down of the grabbed item
                    if (IndexGrabbed != -1) {
                              // place the item on the floor (temporary solution: then we add a better code)
                              // save the crane structure
                              pCrane=GET.pItem;
                              Get(enumGET.ITEM, IndexGrabbed,0);
                              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                              // discover the Y bottom side of grabbed item
                              // (for some items, like the rollingball) it could be different than 0
                              Get(enumGET.ITEM_COLL_BOX, IndexGrabbed,0);
                              // set y coordinate of grabbed item with floor value
                              GET.pItem->CordY = FLOOR.FloorHeight - GET.pCollItem->MaxY;
                              GET.pItem->HeightFloor = FLOOR.FloorHeight;
                              UpdateItemRoom(IndexGrabbed);
                              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                              GET.pItem->HeightFloor = FLOOR.FloorHeight;
                              // restore crane structure
                              GET.pItem = pCrane;
                              // remove data for grabbed item
                              IndexGrabbed=-1;
                              GET.pItem->Reserved_38 = -1;
                    }

In this code we checked the current floor height and then we used that value to move the grabbed object on the floor.
Anyway we used also the collision box of the item because some items (like rolling balls) have not the origin at base of the object but in the middle.
In this situation to discover the Y bottom side of the object we have to read the MaxY value in collision box and move upper in the 3d world (and so, subtracting it) using the MaxY value:

                              Get(enumGET.ITEM, IndexGrabbed,0);
                              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                              // discover the Y bottom side of grabbed item
                              // (for some items, like the rollingball) it could be different than 0
                              Get(enumGET.ITEM_COLL_BOX, IndexGrabbed,0);
                              // set y coordinate of grabbed item with floor value
                              GET.pItem->CordY = FLOOR.FloorHeight - GET.pCollItem->MaxY;

Then we remember to remove the grabbed index from Reserved_38 field, since now crane is no more grabbing any item:

                              GET.pItem->Reserved_38 = -1;


The Crane_UpdateAnimations() function

This function changes the animation of the crane in according with its upward/downward movements.
The code is the same of previous version

void Crane_UpdateAnimations(short ItemIndex)
{
          int NumAnim;

          // handle animations in according with further direction on vertical movments
          if (GET.pItem->Reserved_36 > 0) {
                    // crane is moving down: open the jaws
                    NumAnim = GetRelativeAnimation(GET.pItem);

                    switch (NumAnim) {
                    case 5:
                              // jaws already open: nothing to do
                              break;
                    case 0:
                              // crane has close jaws: set animations to do open them
                              ForceAnimationForItem(GET.pItem, 1, 1);

                              break;
                    default:
                              // all other cases: don't change anything while an animation of change jaws is in progress
                              break;
                    }
          }
          if (GET.pItem->Reserved_36 < 0) {
                    // crane is moving up: close the jaws
                    // at least it was not grabbing an item or the jaws are already close
                    NumAnim = GetRelativeAnimation(GET.pItem);
                    switch(NumAnim)
                    {
                    case 0:
                              // crane is already close: ok, nothing to do
                              break;
                    case 5:
                              // crane is open: close the jaws
                              ForceAnimationForItem(GET.pItem, 2, 0);
                              break;
                    default:
                              // crane is changing its jaws : do not change anything now to don't break an animation in the middle
                              break;
                    }
          }
}


The Crane_MoveCraneHorizontally() function

This function will move the crane horizontally. In this case there is the new about grabbed item.
When the crane is grabbing an item, the function will move the grabbed item following same direction and step of increments

void Crane_MoveCraneHorizontally(short ItemIndex, short OldDirection)
{
          int IndexGrabbed;
          int IncX;
          int IncZ;
          StrItemTr4 *pCrane;

          // move the crane horizontally
          GetIncrements(GET.pItem->Reserved_34, &IncX, &IncZ, 16);
          GET.pItem->CordX += IncX;
          GET.pItem->CordZ += IncZ;
          // update position in the case it changed room
          UpdateItemRoom(ItemIndex);
          CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
          GET.pItem->HeightFloor = FLOOR.FloorHeight;
          // if crane has just changed its direction, we peform a different sound
          if (OldDirection == GET.pItem->Reserved_34) {
                    // it's the same direction: usual moving sound
                    SoundEffect(GetSFX(SFX_CRANE_MOVING, 309), &GET.pItem->CordX, 0);
          }else {
                    // in this moment crane changed direction: using a metallic sound to signal this steering
                    // 312 BLADES_CLASH_LOUD
                    SoundEffect(312, &GET.pItem->CordX,0);
          }
          IndexGrabbed=GET.pItem->Reserved_38;
          // if there is a grabbed item: change also position of grabbed item:
          if (IndexGrabbed != -1) {
                    // save crane structure
                    pCrane= GET.pItem;

                    Get(enumGET.ITEM, IndexGrabbed,0);
                    GET.pItem->CordX += IncX;
                    GET.pItem->CordZ += IncZ;

                    UpdateItemRoom(IndexGrabbed);
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;
                    // restore crane structure
                    GET.pItem = pCrane;
          }
}

We used the same IncX IncZ computed for the crane, to the grabbed item:

          // if there is a grabbed item: change also position of grabbed item:
          if (IndexGrabbed != -1) {
                    // save crane structure
                    pCrane= GET.pItem;

                    Get(enumGET.ITEM, IndexGrabbed,0);
                    GET.pItem->CordX += IncX;
                    GET.pItem->CordZ += IncZ;

                    UpdateItemRoom(IndexGrabbed);
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;
                    // restore crane structure
                    GET.pItem = pCrane;
          }


Testing of grabbing feature

After the updating of new sources, we build the .dll and update it to trle folder.
We have a lot of stuff to test: one test for each object.
As we'll see, there are good new and bad news, of course...


The Big Pillar is good




The crash of Shater Vase is good


When you let falling down the crane over the vase it will be smashed.


The jeep grabbing seems fine


In this case (the picture in the middle) the grabbing was a bit too open, like the crane should be a bit lower to grab better the jeep.


The stop for collision with Grave Chest is good


The crane is not able to grab this static but the collision of static stops the crane at right height.


The grabbing of rollingball is not so good


It has same problem we saw with jeep but in this case it is worse.
Surely the empty collision box around those peaks increased the problem but we should fix it in some manner.


A disaster when we try to grab the jeep from misaligned sector


Look the picture at left of above image...
We moved the crane not over the right sector where is the jeep but on that forward. Surprisingly the crane grabs anyway the jeep and when we lift up it, we see how much is bad this stuff (right picture)
What's happened?
In spite we set an high tollerance to reduce the collision box, own to avoid this problem, the problem happened anyway.
The collision box of the crane touch the collision box the jeep and for this reason it happens the grabbing in spite the two items are misaligned.



Probably the problem occurs only for few game units: a border of crane collision touches a border of jeep collision (see above image)

Really we typed some code in ActionForObject() function, own to avoid this problem:

          // verify if we can grabbing it
          if (TestFast==false && VerticalSpeed != 0) {
                    // crane was moving down slowly: ok
                    // crane and item has to be very closed as pivots to avoid to grab only some its border
                    // with exception if the item has a very large collision box (like jeep)
                    if (          // verify if we can grabbing it
          if (TestFast==false && VerticalSpeed != 0) {
                    // crane was moving down slowly: ok
                    // crane and item has to be very closed as pivots to avoid to grab only some its border
                    // with exception if the item has a very large collision box (like jeep)
                    if (Distance < 256 ) {
                              // ok: grabbing: to see if it's better using the large grabbing or that slim
                              if (MaxSize > 700)           return AFO_GRAB_LARGE;
                              return AFO_GRAB_SLIM;
                    }
          }

When we typed the condition "Distance < 256" we had own this target: allowing the grabbing only when two items are good aligned with a very little difference between their positions.
But in same conditional statement we added also " || MaxSize > 1200", that it means "or at least that the collision box of item to grab, was very big..."
The jeep has a big collision box but now we discover that it's better don't consider this fact: it's necessary that crane and the item was aligned and therefor they had a little distance in horizontal way.
So now we try to fix this bug, changing that condition to remove the " || MaxSize > 1200":

          // verify if we can grabbing it
          if (TestFast==false && VerticalSpeed != 0) {
                    // crane was moving down slowly: ok
                    // crane and item has to be very closed as pivots to avoid to grab only some its border
                    if (Distance < 256) {
                              // ok: grabbing: to see if it's better using the large grabbing or that slim
                              if (MaxSize > 700)           return AFO_GRAB_LARGE;
                              return AFO_GRAB_SLIM;
                    }
          }

Now we build the library and try in game if the bug it has been fixed.




Ok, now the crane is not able to grab the jeep when it is on anothr sector.
Now we should solve the problem about that grabbing in advance that let too far the item, almost it had to fall down.

Improving the grabbing phase


It's easy understand the reason of this bad grabbing: when the crane touches the item, it stops and then it will be started the grabbing animation, but when the crane stops the collision box of the crane has only just touched the item, while we wish that the crane continue to move down to grab very fine the item, since we suppose, that when the jaws are open, there is an empty space inside of the jaws to host the item.
An idea to solve the problem could be this:
  1. When the crane touch a grabbable item:
  2. We don't grab immedietely it, but we set a flag to remember to have started the "grabbing phase".
  3. Then we count some frame yet, while the crane is going on to move down, entering better in the item
  4. Only when a given frame time is elapsed, we'll grab the item

Using above method, the crane could have some frames, moving down, to enter in the item and when we'll grab it, the crane should be lower and grabbing better the item.
The problem now is where saving the frame counter to detect the elapsed time?
We have already used all Reserved fields of crane structure.
An idea to solve the problem is this: we could use as frame counter, own the frame in progress of grabbing animation.


I mean that, we could start grabbing animation like in old code, just we detected first collision, but we don't stop the crane and we let move down yet, and we'll grab really the item (and we'll stop the moving down) only when the grabbing has been completed.
In this way we can check the current progress of grabbing animation like it was our frame counter.
Since the animations to grab are only two:
Animation 3 (next 6) From open to large grabbing
and
Animation 12 (next 8) From open to slim grabbing

Once we check when the next animation begun, the animation 6 or animation 8, we discover that the previous grabbing animation (from open to grabbing) it has been completed and that it will be the moment to grab really the item.

Code to have deep grabbing

We have to add new mnemonic constant for this grabbing phase:

#define CRANE_MOVING_HORIZONTALLY 0x0001
#define CRANE_START_DRIVING 0x0002
#define CRANE_FALLING_DOWN 0x0004
#define CRANE_GRABBING_IN_PROGRESS 0x0008

So we added to the "Constants_mine.h" source the new CRANE_GRABBING_IN_PROGRESS flag.


The changes in Crane_MoveCraneVertically() function
Now we go to the Crane_MoveCraneVertically() function where there will be most of changes.
At begin of the function, we should consider like if the grabbed item was missing when there is the CRANE_GRABBING_IN_PROGRESS flag, because we have not to do move it in this phase.
So we'll change the code from old:

          StrItemTr4 *pCrane;
          bool TestCollision;

          VSpeed = GET.pItem->Reserved_36;
          IndexGrabbed= GET.pItem->Reserved_38;

          // ------ Change vertical speed ----------------
          if (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN) {

To this new version:

          StrItemTr4 *pCrane;
          bool TestCollision;

          VSpeed = GET.pItem->Reserved_36;
          IndexGrabbed= GET.pItem->Reserved_38;
          // if there was the CRANE_GRABBING_IN_PROGRESS flag, it is like the grabbed item was not present
          if (GET.pItem->Reserved_3A & CRANE_GRABBING_IN_PROGRESS) {
                    IndexGrabbed=-1;
          }
          // ------ Change vertical speed ----------------
          if (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN) {

In above code we set as -1 the IndexGrabbed when there is the CRANE_GRABBING_IN_PROGRESS flag.

Always in this function, we'll change the code to detect collisions, from this code:

                              // no grabbed item: check collision between crane and other objects:
                              Flags=AFO_IGNORE;

                              if (IsCollidingWithSomeItem(ItemIndex, GET.pItem->CordX, NewY, GET.pItem->CordZ, GET.pItem->Room,
                                        3072, 250, 256, -128)==true) {
                                        // found a collision: analyse what to do in according with object type
                                        Flags = ActionForObject(ItemIndex, &ObjectTopY, VSpeed);
                                        // restore GET.pItem because ActionForObject() function could have changed it
                                        Get(enumGET.ITEM, ItemIndex,0);
                              }
                              
                              switch (Flags) {
                              case AFO_STOP_ON_COLLISION:

To this version:

                              // no grabbed item: check collision between crane and other objects:
                              Flags=AFO_IGNORE;
                              // if we are in grabbing phase, then we don't check collision and we consider like AFO_IGNORE
                              if ((GET.pItem->Reserved_3A & CRANE_GRABBING_IN_PROGRESS)==0) {
                                        // ok, it's missing the grabbing phase: check the collision:
                                        if (IsCollidingWithSomeItem(ItemIndex, GET.pItem->CordX, NewY, GET.pItem->CordZ, GET.pItem->Room,
                                                  3072, 250, 256, -128)==true) {
                                                  // found a collision: analyse what to do in according with object type
                                                   Flags = ActionForObject(ItemIndex, &ObjectTopY, VSpeed);
                                                   // restore GET.pItem because ActionForObject() function could have changed it
                                                   Get(enumGET.ITEM, ItemIndex,0);
                                        }
                              }
                              switch (Flags) {
                              case AFO_STOP_ON_COLLISION:

Now we perform the check for collisions only if the grabbing phase is missing.

Another change is when we have to begin the grabbing phase:

                              case AFO_GRAB_LARGE:
                              case AFO_GRAB_SLIM:
                                        // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                        GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                        IndexGrabbed= COLLIDE.ItemIndex;
                                        SignalMovedItem(IndexGrabbed);
                                        // set animation of grabbing
                                        if (Flags == AFO_GRAB_LARGE) {
                                                  // from open to large grabbing
                                                  ForceAnimationForItem(GET.pItem, 3, 2);
                                        }
                                        if (Flags == AFO_GRAB_SLIM) {
                                                  // from open to slim grabbing
                                                  ForceAnimationForItem(GET.pItem, 12, 3);
                                        }
                                        // stop vertical movements
                                        VSpeed =0;
                                        break;
                              }

Replacing above code with this:

                              case AFO_GRAB_LARGE:
                              case AFO_GRAB_SLIM:
                                        // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                        GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                        IndexGrabbed= COLLIDE.ItemIndex;
                                        SignalMovedItem(IndexGrabbed);
                                        // set animation of grabbing
                                        if (Flags == AFO_GRAB_LARGE) {
                                                  // from open to large grabbing
                                                  ForceAnimationForItem(GET.pItem, 3, 2);
                                        }
                                        if (Flags == AFO_GRAB_SLIM) {
                                                  // from open to slim grabbing
                                                  ForceAnimationForItem(GET.pItem, 12, 3);
                                        }
                                        // now we have to rememebr that the grabbing phase begun:
                                        GET.pItem->Reserved_3A |= CRANE_GRABBING_IN_PROGRESS;
                                        break;
                              }

We have added the code to set the CRANE_GRABBING_IN_PROGRESS flag, and we removed the code to stop the vertical movement.

Last change for Crane_MoveCraneVertically() function is own when we change the Y coordinate of crane and further grabbed item.
So we change this old code:

          // update values:                    
          GET.pItem->Reserved_36 = VSpeed;
          // compute the differnce in Y coordinate of crane and apply same change for grabbed item
          IncY = NewY - GET.pItem->CordY;
          if (IncY != 0) {
                    // check for change of room
                    GET.pItem->CordY = NewY;
                    UpdateItemRoom(ItemIndex);          
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;
                    // if there is a grabbed item change also its coordinate Y
                    IndexGrabbed=GET.pItem->Reserved_38;
                    if (IndexGrabbed != -1) {
                              // save crane structure
                              pCrane= GET.pItem;

                              // get structure of grabbed item
                              Get(enumGET.ITEM, IndexGrabbed, 0);
                              GET.pItem->CordY += IncY;
                              UpdateItemRoom(IndexGrabbed);
                              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);

                              GET.pItem->HeightFloor = FLOOR.FloorHeight;
                              // restore crane structure
                              GET.pItem = pCrane;
                    }
          }

with this new code:

          // update values:                    
          GET.pItem->Reserved_36 = VSpeed;
          // compute the differnce in Y coordinate of crane and apply same change for grabbed item
          IncY = NewY - GET.pItem->CordY;
          if (IncY != 0) {
                    // check for change of room
                    GET.pItem->CordY = NewY;
                    UpdateItemRoom(ItemIndex);
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;

                    // if there is a grabbed item change also its coordinate Y
                    IndexGrabbed=GET.pItem->Reserved_38;
                    // but it we are in grabbing phase don't move the grabbed item
                    if (GET.pItem->Reserved_3A & CRANE_GRABBING_IN_PROGRESS) {
                              // to do like if the grabbed item was missing:
                              IndexGrabbed=-1;
                    }
                    if (IndexGrabbed != -1) {
                              // save crane structure
                              pCrane= GET.pItem;

                              // get structure of grabbed item
                              Get(enumGET.ITEM, IndexGrabbed, 0);
                              GET.pItem->CordY += IncY;
                              UpdateItemRoom(IndexGrabbed);
                              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                              GET.pItem->HeightFloor = FLOOR.FloorHeight;
                              // restore crane structure
                              GET.pItem = pCrane;
                    }
          }

We added a condition to set -1 IndexGrabbed when the grabbing is in progress.


The changes in Crane_UpdateAnimations() funcion
We said that we discover when the grabbing phase has been completed, checking the current animation of the crane.
When the dynamic from open to grabbing animation has been replaced with fixed grabbing animation, we'll stop the grabbing phase.
So we change the code of Crane_UpdateAnimations() funcion in this way.
From old code:

          // handle animations in according with further direction on vertical movments
          if (GET.pItem->Reserved_36 > 0) {
                    // crane is moving down: open the jaws
                    NumAnim = GetRelativeAnimation(GET.pItem);

                    switch (NumAnim) {
                    case 5:
                              // jaws already open: nothing to do
                              break;
                    case 0:
                              // crane has close jaws: set animations to do open them
                              ForceAnimationForItem(GET.pItem, 1, 1);
                              break;
                    default:
                              // all other cases: don't change anything while an animation of change jaws is in progress
                              break;
                    }
          }

With this new code:

          // handle animations in according with further direction on vertical movments
          if (GET.pItem->Reserved_36 > 0) {
                    // crane is moving down: open the jaws
                    NumAnim = GetRelativeAnimation(GET.pItem);
                    switch (NumAnim) {
                    case 5:
                              // jaws already open: nothing to do
                              break;
                    case 0:
                              // crane has close jaws: set animations to do open them
                              ForceAnimationForItem(GET.pItem, 1, 1);
                              break;
                    case 6:
                    case 8:
                              // crane has fixed grabbing animation (slim or large)
                              // if there was the CRANE_GRABBING_IN_PROGRESS flag: remove it, and stop the vertical movements
                              if (GET.pItem->Reserved_3A & CRANE_GRABBING_IN_PROGRESS) {
                                        GET.pItem->Reserved_3A &= ~CRANE_GRABBING_IN_PROGRESS;

                                        GET.pItem->Reserved_36 =0;
                              }
                              break;
                    default:
                              // all other cases: don't change anything while an animation of change jaws is in progress
                              break;
                    }
          }

We added the new cases: 6 and 8, the fixed animations with grabbing.
When crane reached these animations and there was grabbing phase in progress, we'll remove the grabbing flag and stop the moving down movement.


Changes in ControlCraneManual() function
To avoid that some input command, sent own in this short moment of grabbing phase, messed up our grabbing, we change the condition to check for input commands, to exlcude any command when we are in grabbing phase.
So we'll change this code:

          // discover if the crane is at center of current sector.
          // only in this situation we could accept many game commands to change direction, open/close jaws, move up/down ect.
          if (FLOOR.SectorCoords.Radius < 15 &&
                    (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN)==0 ) {

                    // -------- INPUT COMMANDS TO MOVE VERTICALLY THE CRANE ---------------------------

In this way:

          // discover if the crane is at center of current sector.
          // only in this situation we could accept many game commands to change direction, open/close jaws, move up/down ect.
          if (FLOOR.SectorCoords.Radius < 15 &&
                    (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN)==0 &&
                    (GET.pItem->Reserved_3A & CRANE_GRABBING_IN_PROGRESS)==0) {

                    // -------- INPUT COMMANDS TO MOVE VERTICALLY THE CRANE ---------------------------

Pratically we added another condition to receive commands when the CRANE_GRABBING_IN_PROGRESS flag is missing.

Testing new deep grabbing



Comparing the old grabbing (at left in above image) with new deep grabbing (at right) we see that now it works better.
The jaws moved lower and more volume of the jeep is grabbed from the crane.
An interesting matter... since the extra moving down it has been allowed by the durate of grabbing animation, this means that, if we increase that animation or increase its frame rate value, we'll able to get yet more strong this extra movement.


Easy grabbing or hard grabbing?

Now the code for grabbing is working enough fine, anyway we forgot to use the game command: "WALK(shift): Try to close the jaws to grab some item."
Since now it happens that the grabbing is automatic.
When the crane touches the item, the code perform itself the grabbing animation and then the player has only to give the command to move up the crane with the grabbed item.
It's not so bad, but we have not given any chance to use the grab input command WALK(shift).
More, perhaps it's a bit too easy to be fun.
I mean that it could be more interesting performing some action in game where it required some playing skill to realize it.

An idea is this: we could keep this grabbing mode but optionally (via ocb settings) we could think also to another way to grab the items. An harder way, where the player should use the grab command in right moment otherwise the grabbing it will fail.


The hard grabbing mode

To support the new grabbing mode, we need of new ocb value for crane:

// Ocb flags for swinging crane
#define CRANE_OCB_AUTOMATIC 0x0001
#define CRANE_OCB_MANUAL 0x0002
#define CRANE_OCB_SHADOW 0x0004
#define CRANE_OCB_PROP_SHADOW 0x0008
#define CRANE_OCB_HARD_GRABBING 0x0010

And now we have to plan as it should work...
We should set a valid time range whom the player should send the grab (WALK) command. If the command comes in this valid range the grabbing will work, otherwise there will be a fail.
To count this time we should use newly the frame of grabbing animation but this time it will be the player to perform it in right moment.
This means that the player should hit the grab command just a moment before of detection of colliding, and more precisely:
When the code detect the collision with a grabbable item the grabbing animation 3 should be in progress. In this case the grabbing will work.
While... if the player hit the grab command too early, when the crane will collide with the item, the animation 3 will be already completed and the current animation will be that next, the animation 6.
And if the player hit the grab command too late, when the collision is already happened, the code it will have already forced the animation 2 (from open to close the jaws) and in this case the command of grab it will be not accepted.
This means that will accept the grab command only when the animation is the animation 5 (always open jaws).

The code to handle hard grabbing

We'll have to add little changes in different functions.
Most of these changes will have as first intruction a condition to verify that in Ocb field of the crane there was the new CRANE_OCB_HARD_GRABBING setting. When that setting will be missing we'll use only old code.

The changes in Crane_MoveCraneVertically() function for hard grabbing

First change is when we detect the collision with some grabbable item.
In that situation we'll check if there is the new hard grabbing and in if it there is, we'll check also if current condition if that right: animation 3 (the crane is from open to grab):

                              case AFO_GRAB_LARGE:
                              case AFO_GRAB_SLIM:
                                        // if there is hard grabbing, verify if there is right animation in progress
                                        if (GET.pItem->OcbCode & CRANE_OCB_HARD_GRABBING) {
                                                  if (GetRelativeAnimation(GET.pItem) == 3) {
                                                            // animation is right
                                                            GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                            IndexGrabbed= COLLIDE.ItemIndex;
                                                            SignalMovedItem(IndexGrabbed);
                                                            // now we have to rememebr that the grabbing phase begun:
                                                            GET.pItem->Reserved_3A |= CRANE_GRABBING_IN_PROGRESS;
                                                            // verify if it requires slim grabbing
                                                            if (Flags == AFO_GRAB_SLIM) {
                                                                      // yes, set flag to remember that requires slim grabbing
                                                                      GET.pItem->Reserved_3A |= CRANE_REQUIRES_SLIM_GRAB;          
                                                            }
                                                  }else {
                                                            // animation is wrong: to do fail the grabbing
                                                            // consider item only as floor collision
                                                            FloorY=ObjectTopY;
                                                  }
                                                  break;
                                        }

                                        // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                        GET.pItem->Reserved_38 = COLLIDE.ItemIndex;

Here we have set also the new flag for crane status: CRANE_REQUIRES_SLIM_GRAB
We defined it in "constants_mine.h" source:

#define CRANE_GRABBING_IN_PROGRESS 0x0008
#define CRANE_REQUIRES_SLIM_GRAB 0x0010

And we use it to remember what kind of grabbing (slim or large) it required for the item is colliding with the crane.
Since we'll begin always the grabbing with animation 3 (from open to large grabbing) when it has been required the slim grabbing, we'll have to wait that the large grabbing has been reached (with animation 6) and then force the animation 7 to change from large grabbing to slim grabbing. (See changes in Crane_UpdateAnimations() function)


Please note that, when there is the ocb for hard grabbing but the animation is wrong, the code will perform:

                                                  }else {
                                                            // animation is wrong: to do fail the grabbing
                                                            // consider item only as floor collision
                                                            FloorY=ObjectTopY;
                                                  }

In this way, the next code (at end of current switch() staement) it will apply common collision to the grabbable item, like it was a static.


The changes in Crane_InputOthers() function for hard grabbing

In this function we have to check if the input command for grabbing it has been sent, but we'll perform this check only when there is ocb for hard grabbing:

          // check for "Grab item" command but only if it is enabled the hard grabbing
          if (GET.pItem->OcbCode & CRANE_OCB_HARD_GRABBING) {
                    if (FlagsInput & enumCMD.WALK ) {
                              // accept this command only when the current animation is 5 (always open jaws)
                              if (GetRelativeAnimation(GET.pItem) == 5) {
                                        // ok: set animation to grab
                                        ForceAnimationForItem(GET.pItem, 3, 2);
                              }
                    }
          }
          // check for "Open Jaws" command:


The changes in Crane_UpdateAnimations() function for hard grabbing

Here we have to detect if it has been required a slim grabbing (checking for the CRANE_REQUIRES_SLIM_GRAB flag) and if it has been, and currently there is animation 6 (always large grabbing) we'll force the change from large to slim grabbing, with animation 7.

          // handle animations in according with further direction on vertical movments
          if (GET.pItem->Reserved_36 > 0) {
                    // crane is moving down: open the jaws
                    NumAnim = GetRelativeAnimation(GET.pItem);

                    switch (NumAnim) {
                    case 5:
                              // jaws already open: nothing to do
                              break;

                    case 0:
                              // crane has close jaws: set animations to do open them
                              ForceAnimationForItem(GET.pItem, 1, 1);
                              break;
                    case 6:
                              // large grabbing
                              /// if required slim grabbing: set animation for slim grabbing
                              if (GET.pItem->Reserved_3A & CRANE_REQUIRES_SLIM_GRAB) {
                                        ForceAnimationForItem(GET.pItem, 7, 3);
                                        // now remove the flag to avoid mess in future with other attempts of grabbing
                                        GET.pItem->Reserved_3A &= ~CRANE_REQUIRES_SLIM_GRAB;
                              }
                              // now the code will continue with "case 8:" (no break;)
                    case 8:
                              // crane has fixed grabbing animation (slim or large)
                              // if there was the CRANE_GRABBING_IN_PROGRESS flag: remove it, and stop the vertical movements


Another change in same function is to force close jaws when the crane is moving up with no grabbed item.
It already happened but now we have also to manage the chance that current animation was not "open jaws" but "grabbing jaws" for a failed attempt to grab an item

          if (GET.pItem->Reserved_36 < 0) {
                    // crane is moving up: close the jaws

                    // at least it was not grabbing an item or the jaws are already close
                    NumAnim = GetRelativeAnimation(GET.pItem);
                    switch(NumAnim)
                    {
                    case 0:
                              // crane is already close: ok, nothing to do
                              break;
                    case 5:
                              // crane is open: close the jaws
                              ForceAnimationForItem(GET.pItem, 2, 0);
                              break;
                    case 6:
                              // crane has large grabbing
                              // and it's moving up
                              // if there is no grabbed item: force the full close jaws

                              // if there is no grabbed item or grabbing in progress: to do close newly the jaws
                              if ((GET.pItem->Reserved_3A & CRANE_GRABBING_IN_PROGRESS)==0 &&
                                        GET.pItem->Reserved_38 == -1) {
                                        ForceAnimationForItem(GET.pItem, 10, 0);
                              }
                              break;
                    default:


Testing new hard grabbing

The ocb code for hard grabbing is "0x10" i.e. 16. Since we have to set also "2" for manual crane a valid number should be 18 to enable the hard grabbing.
Once you updated library and level project (but the hard grabbing should be always enabled in ocb code), you can can test in game how it work.




It's not so clear from images, anyway it seems work fine.
In first two pictures (A and B) you see two moment of same failed attempt. At end the crane is closed over the pillar, only because I gave the "grab" command a bit too early that required.
While in C picture you see that it is possible grabbing it correctly after some attempt.


Improve the "release item" phase

Now we should make a speech about the release item phase.
Once we grabbed an item we can move it and then we'll release it using the "open jaws" (DRAW WEAPONS) command.
We have to set if we mean to let the chance to left the grabbed item when the crane is far from floor, and in this case we should make a code to simulate this falling down of the grabbed item.
In the case we wish forbid this chance we should ignore "open jaws" command when the grabbed item is not yet touching the floor.
While if we accept this chance we should manage this falling down in realistic way.
Probably it's not very useful having this chance to get falling down the item from big ground elevation, anyway, since we are trying to explore mostly all plugin skills, we'll manage also this chance, giving to the level builder the decision if enabling it (or less) in game.
So we'll create another ocb setting to enable the chance to release in the empty the grabbed item:

// Ocb flags for swinging crane
#define CRANE_OCB_AUTOMATIC 0x0001
#define CRANE_OCB_MANUAL 0x0002
#define CRANE_OCB_SHADOW 0x0004
#define CRANE_OCB_PROP_SHADOW 0x0008
#define CRANE_OCB_HARD_GRABBING 0x0010
#define CRANE_OCB_FALLING_DOWN_ITEM 0x0020

So, when in the ocb code of crane, the CRANE_OCB_FALLING_DOWN_ITEM flag is present, the player will be able to release the item from ground elevations, while when it is missing, the command to open jaws (with grabbed item inside) will be disabled when item is not touching the floor.


How to manage a parallel process

Thinking about the falling down, we have the problem to manage all process of its falling, since it could require some time and a simulation of gravity, other that further collisions with other items.
The problem is that it should be very complicated (and messed) manage this code in crane functions, since once the item it has been released, the crane will have no more relation with that old grabbed item.
When we neeed begin a long and complicate process, from another process, the best solution is to create a progressive action to manage this child process.
Pratically the crane code will simply start this new progressive action, but then it (the crane code) will ignore that matter, because it will be only a target of progressive action to handle it.
In our case, we could create a progressive action that had as target to move down, until to reach the floor, a moveable item, simulating gravity and managing collisions with other moveables.


How to initialise a new progressive action

Everytime we create a new progressive action we have to do following operations:
  1. Define a new AXN_ constant to indenfy the new action

  2. To set what, where and how many values to save in the structure of this progressive action

  3. Get a new free progressive action structure with the instruction:

    Get(enumGET.PROGRESSIVE_ACTION, 0,0);

  4. Copy the AXN_ value in "ActionType" field and then set also other fields with global values you need to manage this action, using a syntax like:

              GET.pAction->ActionType = AXN_ADD_EFFECT;
              GET.pAction->Arg1 = 30; // 30 frames
              GET.pAction->Arg2 = EFF_ADD_SPARKS;
              GET.pAction->ItemIndex = ItemIndex;

  5. Add a "case AXN....:" statement to the code of PerformMyProgrAction() function

  6. Type the code to manage this action, enclosing it between the "case AXN_..:" and the final "break;" statements

  7. In this code, remember to detect when the action it has been completed, when it happens, delete it, with the instruction:

              pAction->ActionType= AXN_FREE;



The progressive action to do falling down items

We create a new constant for our progressive action:

#define AXN_MOVE_CUBE 1
#define AXN_ADD_EFFECT 2
#define AXN_FALLING_DOWN_ITEM 3


Since this progressive action it is independent by crane, we could use it in future also for other targets, so we create a litle function that had the target to initialise and start this progressive action.

The CreateFallingDownForItem() function


// create a progressive action to do falling down the moveable with index = ItemIndex
// from current position, until it reaches the floor
// arguments: IndexItemToIgnore = index of another moveale whose ignore further collision with main index (or -1)
//                               VSpeedStart = beginning vertical speed, VSpeedAcc = acceleration of vertical speed,
//                               VSpeedMax = max value for vertical speed (limit to do no pass over)
// SoundForCrash = sound effect to play when item touch the floor (or -1 if no sound)
// Flags = flags of FDI (FallingDownItem) type to customize the behaviour of this action
void CreateFallingDownForItem(short ItemIndex, int IndexItemToIgnore, int VSpeedStart, int VSpeedAcc,
                                                                       int VSpeedMax, int SoundForCrash, WORD Flags)
{
          
          Get(enumGET.PROGRESSIVE_ACTION, 0,0);

          // fill action structure:
          // action type:
          GET.pAction->ActionType = AXN_FALLING_DOWN_ITEM;

          // index of moveable to move:
          GET.pAction->ItemIndex = ItemIndex;

          // durate, unknown, and so endless as frame counter:
          GET.pAction->Arg1 = ENDLESS_DURATE;

          // initialise global values we'll use to simulate gravity
          GET.pAction->VetArg[0] = VSpeedStart; // beginning value for vertical speed
          GET.pAction->VetArg[1] = VSpeedAcc; // acceleration







          GET.pAction->VetArg[2] = VSpeedMax; // max limit for vertical speed
          GET.pAction->VetArg[3] = SoundForCrash; // sound effect to play
          GET.pAction->VetArg[4] = IndexItemToIgnore; // index of item to ignore in colliding analys
          GET.pAction->Arg2 = Flags; // Flags FDI

          SignalMovedItem(ItemIndex);
}

We create a new kind of flag: FDI (Falling Down for Item) to customize some setting of this action.
Here we have all their definitions:

// flags FDI to customize FallingDownItem action
#define FDI_NULL 0x0000
#define FDI_EARTH_QUAKE 0x0001 // perform eartquake when item touches the floor
#define FDI_ENGAGE_ROLLINGBALL 0x0002 // if item is a rollingball, engage it
#define FDI_EXPLODE_ITEM 0x0004 // to do explode the item when it touches the ground
#define FDI_KILL_ENEMY 0x0008 // if item touch creatures: kill them

Please note we used in this function the call:

          SignalMovedItem(ItemIndex);


Because it's better perform only once this operation and not in any cycle of progressive action when it will be performed.
The many paraemters will allow to change speed and acceleration and other behaviours with FDI flags. In this way we could use different setting in according with the kind of operation we are making.

The code for AXN_FALLING_DOWN_ITEM progressive action

In the PerformMyProgrAction() function, we'll add the new case for our AXN_FALLING_DOWN_ITEM.
It will be called for each frame and in this way we can handle independently way this code.

          switch (pAction->ActionType) {
                    
          case AXN_FALLING_DOWN_ITEM:
                    // perform falling down of the item, until it reaches floor or other object
                    ItemIndex = pAction->ItemIndex;

                    Get(enumGET.ITEM, ItemIndex, 0);

                    NewY= GET.pItem->CordY;

                    // apply to Y coordinate the current vertical speed
                    NewY += pAction->VetArg[0];
                    // discover the height of the floor below current item
                    CheckFloor(GET.pItem->CordX, NewY, GET.pItem->CordZ, GET.pItem->Room);
                    FloorY=FLOOR.FloorHeight;

                    // if there is an item to ignore: disable now its collisions
                    IndexToIgnore = pAction->VetArg[4];
                    if (IndexToIgnore != -1) {
                              // save item structure of main item
                              pItem = GET.pItem;
                              // get data about item to ignore
                              Get(enumGET.ITEM, IndexToIgnore,0);
                              Get(enumGET.SLOT, GET.pItem->SlotID,0);

                              // save its collision procedure
                              pColl = GET.pSlot->pProcCollision;
                              // set null its collision procedure
                              GET.pSlot->pProcCollision = NULL;
                              // restore structure of main item
                              GET.pItem = pItem;
                    }
                    // verify if there is a collision between item and other items
                    if (IsCollidingWithSomeItem(ItemIndex, GET.pItem->CordX, NewY, GET.pItem->CordZ,
                              GET.pItem->Room, 3072, 128, 128, -128)==true) {
                              if (pAction->Arg2 & FDI_KILL_ENEMY) {
                                        // set to kill creature
                                        TestKill=true;
                              }else {
                                        TestKill=false;
                              }
                              // save structure of item:
                              pItem = GET.pItem;
                              Flags = ActionForObject(ItemIndex, &TopY, pAction->VetArg[0], false, TestKill);

                              if (Flags == AFO_STOP_ON_COLLISION) {
                                        FloorY= TopY;
                              }
                              // restore item structure
                              GET.pItem = pItem;
                    }
                    // if there was item to ignore, restore its collision procedure
                    if (IndexToIgnore != -1) {
                              // save item structure of main item
                              pItem = GET.pItem;
                              // get data about item to ignore
                              Get(enumGET.ITEM, IndexToIgnore,0);
                              Get(enumGET.SLOT, GET.pItem->SlotID,0);
                              // restore its collision procedure
                              GET.pSlot->pProcCollision = pColl;
                              
                              // restore structure of main item
                              GET.pItem = pItem;
                    }
                    // discover if item touched the floor (or top collision of some item)
                    // compute the lowest Y coordinate of current item
                    Get(enumGET.ITEM_COLL_BOX, ItemIndex,0);
                    BaseY = NewY + GET.pCollItem->MaxY;
                    if (BaseY >= FloorY) {
                              // reached the floor
                              // force Y coordinate to be aligned with floor
                              NewY = FloorY - GET.pCollItem->MaxY;
                              // perform sound effect?
                              if (pAction->VetArg[3] != -1) {
                                        SoundEffect(pAction->VetArg[3], &GET.pItem->CordX, 0);
                              }
                              // perform earth quake?
                              if (pAction->Arg2 & FDI_EARTH_QUAKE) {
                                        PerformFlipeffect(NULL, 1, 0, 0);
                              }

                              // to do explode the item?
                              if (pAction->Arg2 & FDI_EXPLODE_ITEM) {
                                        // use action trigger 14 to do explode moveable (different than creature)
                                        PerformActionTrigger(NULL, 14, ItemIndex, 2);
                              }

                              // engage the item if it was a rollingball
                              if (GET.pItem->SlotID == enumSLOT.ROLLINGBALL) {
                                        if (pAction->Arg2 & FDI_ENGAGE_ROLLINGBALL) {
                                                  // trigger the rollingball
                                                  PerformActionTrigger(NULL, 43, ItemIndex, 0);
                                        }
                              }
                              // disable this progressive action
                              pAction->ActionType = AXN_FREE;
                    }
                    // Update position of item
                    GET.pItem->CordY = NewY;
                    UpdateItemRoom(ItemIndex);
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;
                    // change vertical speed with acceleration
                    pAction->VetArg[0] += pAction->VetArg[1];
                    // verify to have not reached max limit for vertical speed
                    if (pAction->VetArg[0] > pAction->VetArg[2]) {
                              // we have passed over the max limit: force vertical speed to max limit
                              pAction->VetArg[0] = pAction->VetArg[2];
                    }
                    break;

Above code requires some local variable that we'll define at top and inside of same PerformMyProgrAction() function, of course:

          int NewY;
          int ItemIndex;
          int FloorY;
          StrItemTr4 *pItem;
          int TopY;
          int BaseY;
          DWORD Flags;
          void *pColl;
          int IndexToIgnore;
          bool TestKill;

Looking above code, we used newly the IsCollidingWithSomeItem() function to detect the collision between the falling item and other objects.
Anyway more interesting is that we used yet also the ActionForObject() function to handle the further collision.
The ActionForObject() function had been typed for crane vertical movement and indeed, it has some specific code to detect some flag of crane, like the CRANE_FALLING_DOWN flag.
We chose to use it also for this progressive action, because the target of that function was almost the same we need in this circustance: detect if the item is a shatter (to smash it) or a creature (to kill it) or a static whose using only the collision box to stop our falling item.
So, rather to create another function that worked almost in same way, we prefered restyle a bit that old ActionForObject() function, to get possible using it also for other targets, different than crane movements.
Now we see how we adapted that old code to have a more general code...

The changes in ActionForObject() function

As first step, we changed the input parameter, adding a TestCrane to know if it has to do work for the crane, on in more general way:

DWORD ActionForObject(short ItemIndex, int *pCollisionYCoord, int VerticalSpeed,
                                                   bool TestCrane, bool TestKill)

We added also the input argument: TestKill
It will be used only when TestCrane == false, to discover if the creatures should be killed or less.

The new code for ActionForObject() function


DWORD ActionForObject(short ItemIndex, int *pCollisionYCoord, int VerticalSpeed, bool TestCrane, bool TestKill)
{
          int SizeX;
          int SizeZ;
          StrBoxCollisione *pColl;
          bool TestStatic;
          StrPosizione Pos;
          StrItemTr4 *pCrane;
          bool TestFast;
          int Distance;
          int MinSize;
          int MaxSize;
          int TopY;
          int MainItemTopY;
          
          // save the crane structure in pCrane because we need of GET.pItem for further moveable with whom there is the collision
          pCrane= GET.pItem;
          
          // item could be a moveable or a static: try to get them data in common variables
          if (COLLIDE.ItemIndex != -1) {
                    // it is a moveable
                    Get(enumGET.ITEM_COLL_BOX, COLLIDE.ItemIndex,0);
                    pColl= GET.pCollItem;
                    // get position of item
                    Get(enumGET.ITEM, COLLIDE.ItemIndex, 0);
                    Pos.OrgX = GET.pItem->CordX;
                    Pos.OrgY = GET.pItem->CordY;
                    Pos.OrgZ = GET.pItem->CordZ;

                    TestStatic=false;
          }else {
                    // it is a static
                    Get(enumGET.STATIC_COLL_BOX, COLLIDE.StaticIndexRoom, COLLIDE.StaticIndex);
                    pColl= GET.pCollStatic;
                    // get position of static
                    Get(enumGET.STATIC, COLLIDE.StaticIndexRoom , COLLIDE.StaticIndex );
                    Pos.OrgX = GET.pStatic->x;
                    Pos.OrgY = GET.pStatic->y;
                    Pos.OrgZ = GET.pStatic->z;
                    TestStatic=true;
          }
          // set the Y coordinate of top side
          TopY = Pos.OrgY + pColl->MinY;
          *pCollisionYCoord = TopY;
          // if the topy is over the top side of main item: ignore the collision
          if (VerticalSpeed > 0) {
                    // crane is moving down
                    Get(enumGET.ITEM_COLL_BOX, ItemIndex, 0);
                    MainItemTopY = pCrane->CordY + GET.pCollItem->MinY;

                    if (TopY < MainItemTopY) {
                              // the item is colliding from above while the crane is moving down: ignore that upper item collision
                              return AFO_IGNORE;
                    }
          }
          // discover size of both sides
          SizeX= pColl->MaxX - pColl->MinX;
          SizeZ= pColl->MaxZ - pColl->MinZ;
          // the IsCollideWithSomeItem() function, had already excluded item too little, but now we
          // have to exclude from grabbing also the items too slim in their littler side

          // discover size of shorter side
          MinSize= SizeX;
          if (SizeZ < MinSize) MinSize = SizeZ;

          // discover length of larger side
          MaxSize= SizeX;
          if (SizeZ > MaxSize) MaxSize=SizeZ;

          // compute the distance between two origins
          Distance=GetDistanceXZ(pCrane->CordX, pCrane->CordZ, Pos.OrgX, Pos.OrgZ);
          
          // discover if crane felt down: in this case there is an high vertical speed
          if (TestCrane==true) {
                    // use specific of crane:
                    if (pCrane->Reserved_3A & CRANE_FALLING_DOWN) {
                              TestFast=true;
                    }else {
                              TestFast=false;
                    }
          }else {
                    // it's not for crane but generic item collisions: detect fast speed by current VSpeed
                    if (VerticalSpeed >=64) {
                              TestFast=true;
                    }else {
                              TestFast=false;
                    }
          }
          // now we have many data: discover the action to perform

          // ---------- ANALYSE FOR STATICS --------------------------
          if (TestStatic==true) {
                    // discover if static is a shatter
                    if (GET.pStatic->SlotId >= Trng.pGlobTomb4->pBaseCustomize->ShatterInizio &&
                              GET.pStatic->SlotId <= Trng.pGlobTomb4->pBaseCustomize->ShatterFine) {
                              
                              // it's a shatter: destroy it but only if there is falling down of crane
                              if (TestFast==true) {
                                        DestroyShatterObject(COLLIDE.StaticIndex, COLLIDE.StaticIndexRoom);
                                        return AFO_SHATTER_OBJECT;
                              }
                    }
                    // it's not possible destroying it.
                    // if its position is very closed to crane position (center pivot for both) and
                    // the minsize of static is very little, we can ignore it
                    if (MinSize < 256 && Distance < 128) {
                              // ignore it!
                              return AFO_IGNORE;
                    }

                    // in other situation: we consider it like a floor collision to stop the crane before touching the floor
                    return AFO_STOP_ON_COLLISION;
          }
          // ---------- ANALYSE FOR MOVEABLES -------------------------
          // discover if the moveable is a creature
          Get(enumGET.INFO_ITEM, COLLIDE.ItemIndex,0);
          if (GET.InfoItem.TestCreature == true) {
                    // the crane will injury or killing creature only if there is
                    // a verticalspeed or there is animation 2 (it is "biting")
                    if (TestCrane==true && (VerticalSpeed != 0 || GetRelativeAnimation(pCrane) == 2) ||
                              TestCrane==false && TestKill==true) {

                              // yes it is a creature
                              if (GET.InfoItem.TestSemiGod == true || GET.InfoItem.TestOnlyExplode == true) {
                                        // it is a semi-god or requires explosive ammo
                                        // if there is fast speed kill him anyway
                                        if (TestFast==true) {
                                                  KillSemigod(COLLIDE.ItemIndex);
                                                  return AFO_KILL_CREATURE;
                                        }
                                        // not fast: push away it
                                        ItemPushAwayItem(ItemIndex, COLLIDE.ItemIndex);
                                        return AFO_PUSH_AWAY_CREATURE;
                              }
                              // it's a semigod:
                              // if crane is falling down: kill immedialty
                              if (TestFast==true) {
                                        GET.pItem->Health=0;
                                        return AFO_KILL_CREATURE;
                              }
                              if (GET.InfoItem.TestOnlyExplode == false) {
                                        GET.pItem->Health -= 10;
                              }
                    }
                    // crane is moving slowly push aways the creature

                    ItemPushAwayItem(ItemIndex, COLLIDE.ItemIndex);
                    return AFO_INJURY_CREATURE;
          }
          // it's not a creature:
          // discover if we can ignore it

          // if its position is very closed to crane position (pivots, at center, aligned) and
          // the minsize of static is very little, we can ignore it
          if (MinSize < 256 && Distance < 128) {
                    // ignore it!
                    return AFO_IGNORE;
                    }
          // verify if we can grabbing it
          if (TestCrane==true && TestFast==false && VerticalSpeed != 0) {
                    // crane was moving down slowly: ok
                    // crane and item has to be very closed as pivots to avoid to grab only some its border
                    if (Distance < 256) {
                              // ok: grabbing: to see if it's better using the large grabbing or that slim
                              if (MaxSize > 700)           return AFO_GRAB_LARGE;
                              return AFO_GRAB_SLIM;
                    }
          }
          // last chance: consider it as collision
          return AFO_STOP_ON_COLLISION;
}

We did also an improvement that doesn't affect the speech about conversion from "crane specific" to "general collision analyses".

          // set the Y coordinate of top side
          TopY = Pos.OrgY + pColl->MinY;
          *pCollisionYCoord = TopY;
          // if the topy is over the top side of main item: ignore the collision
          if (VerticalSpeed > 0) {
                    // crane is moving down
                    Get(enumGET.ITEM_COLL_BOX, ItemIndex, 0);
                    MainItemTopY = pCrane->CordY + GET.pCollItem->MinY;

                    if (TopY < MainItemTopY) {
                              // the item is colliding from above while the crane is moving down: ignore that upper item collision
                              return AFO_IGNORE;
                    }
          }

In above code we consider the chance that, the item is colliding with our main item (crane or less), had an higher position in 3d world respect to the main item.
In this situation is not logic that a collision at top of main item, stop its moving down.
So we compared the top Y coordinate of main item with that of collided item, and when the collided item is above the main item, we'll ignore this collision.

To discover the fast speed (TestFastSpeed) we used two different way, depending on we are working with crane or less:

          // discover if crane felt down: in this case there is an high vertical speed
          if (TestCrane==true) {
                    // use specific of crane:
                    if (pCrane->Reserved_3A & CRANE_FALLING_DOWN) {
                              TestFast=true;
                    }else {
                              TestFast=false;
                    }
          }else {
                    // it's not for crane but generic item collisions: detect fast speed by current VSpeed
                    if (VerticalSpeed >=64) {
                              TestFast=true;
                    }else {
                              TestFast=false;
                    }
          }


Also to set if killing or less creature, we used two dfferent conditions:

          // ---------- ANALYSE FOR MOVEABLES -------------------------
          // discover if the moveable is a creature
          Get(enumGET.INFO_ITEM, COLLIDE.ItemIndex,0);
          if (GET.InfoItem.TestCreature == true) {
                    // the crane will injury or killing creature only if there is
                    // a verticalspeed or there is animation 2 (it is "biting")
                    // when main item is not crane, we'll use input argument TestKill to set if kill or less the creature
                    if (TestCrane==true && (VerticalSpeed != 0 || GetRelativeAnimation(pCrane) == 2) ||
                              TestCrane==false && TestKill==true) {

                              // yes it is a creature
                              if (GET.InfoItem.TestSemiGod == true || GET.InfoItem.TestOnlyExplode == true) {


When we are working with crane (TestCrane == true), we'll use old method to detect the animation for biting,
while it is not crane, we'll use the TestKill parameter.

When it is not a crane the main item, we'll not consider the chance to grab an item, of course:

          // verify if we can grabbing it
          if (TestCrane==true && TestFast==false && VerticalSpeed != 0) {
                    // crane was moving down slowly: ok
                    // crane and item has to be very closed as pivots to avoid to grab only some its border
                    if (Distance < 256) {
                              // ok: grabbing: to see if it's better using the large grabbing or that slim
                              if (MaxSize > 700)           return AFO_GRAB_LARGE;
                              return AFO_GRAB_SLIM;
                    }
          }

So we added to the condition the "TestCrane==true" because only when we are working with the crane the grabbing is possible.


Changes in Crane_MoveCraneVertically() function

We mean handle the falling down of the item, but in our new code we'll set also the code to handle the opposite situation: when the CRANE_OCB_FALLING_DOWN_ITEM ocb is missing.
In this situation, it will be forbidden open jaws when there is a grabbed item and this is yet floating in the empty. Only when the grabbed item is touching the floor it will be possible open the jaws.
To handle this situation, we should know when the grabbed item is touching the floor, and the easier solution is to use old code, when the crane is moving down with grabbed item, detecting when the item touch the floor, and set a new flag in Reserved_3A to remember that we are in right situation.
We define it togheter with other crane flags:

// flags for manual crane (stored in Reserved_3A field of crane structure)
#define CRANE_MOVING_HORIZONTALLY 0x0001
#define CRANE_START_DRIVING 0x0002
#define CRANE_FALLING_DOWN 0x0004
#define CRANE_GRABBING_IN_PROGRESS 0x0008
#define CRANE_REQUIRES_SLIM_GRAB 0x0010
#define CRANE_ITEM_ON_FLOOR 0x0020

Then we'll have to remove this flag when the crane is moving up, because now the vertical position it will be different.
So, in the code where crane is moving up, we'll remove the CRANE_ITEM_ON_FLOOR flag:

          // check if crane reached some obstacle:
          if (VSpeed < 0) {
                    // crane was moving up: check if it reached ceiling
                    if (NewY <= BaseCeilingY) {
                              // yes: reached ceiling: stop the movement
                              VSpeed=0;
                              NewY = BaseCeilingY;
                    }
                    // moving up, the further grabbed item is surely no more over the floor:
                    GET.pItem->Reserved_3A &= ~CRANE_ITEM_ON_FLOOR;
          }

While when the crane is moving down, in the code where we detect the floor, we'll add the CRANE_ITEM_ON_FLOOR flag:

                              // if there is collision: stop here
                              if (TestCollision) {
                                        // restore old Y coordinate of crane
                                        NewY= GET.pItem->CordY;
                                        // and clear vertical speed
                                        VSpeed=0;
                                        // inform that item reached the floor
                                        GET.pItem->Reserved_3A |= CRANE_ITEM_ON_FLOOR;
                              }

We should add this flag, also when the crane has just grabbed the item, because in this moment the item is yet on the floor:

                              case AFO_GRAB_LARGE:
                              case AFO_GRAB_SLIM:
                                        // if there is hard grabbing, verify if there is right animation in progress
                                        if (GET.pItem->OcbCode & CRANE_OCB_HARD_GRABBING) {
                                                  if (GetRelativeAnimation(GET.pItem) == 3) {
                                                            // animation is right
                                                            GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                            IndexGrabbed= COLLIDE.ItemIndex;
                                                            SignalMovedItem(IndexGrabbed);
                                                            // now we have to rememebr that the grabbing phase begun:
                                                            GET.pItem->Reserved_3A |= CRANE_GRABBING_IN_PROGRESS;
                                                            // verify if it requires slim grabbing
                                                            if (Flags == AFO_GRAB_SLIM) {
                                                                      // yes, set flag to remember that requires slim grabbing
                                                                      GET.pItem->Reserved_3A |= CRANE_REQUIRES_SLIM_GRAB;          
                                                            }
                                                            // set that currently the grabbed item is yet on the floor
                                                            GET.pItem->Reserved_3A |= CRANE_ITEM_ON_FLOOR;
                                                  }

Above is the code for hard grabbing, and then we do the same for the code with automatic grabbing:

                                        // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                        GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                        IndexGrabbed= COLLIDE.ItemIndex;
                                        SignalMovedItem(IndexGrabbed);
                                        // set animation of grabbing
                                        if (Flags == AFO_GRAB_LARGE) {
                                                  // from open to large grabbing
                                                  ForceAnimationForItem(GET.pItem, 3, 2);
                                        }

                                        if (Flags == AFO_GRAB_SLIM) {
                                                  // from open to slim grabbing
                                                  ForceAnimationForItem(GET.pItem, 12, 3);
                                        }

                                        // now we have to rememebr that the grabbing phase begun:
                                        GET.pItem->Reserved_3A |= CRANE_GRABBING_IN_PROGRESS;
                                        // set that currently the grabbed item is yet on the floor
                                        GET.pItem->Reserved_3A |= CRANE_ITEM_ON_FLOOR;

                                        break;


We have changed also the call to ActionForObject() function, since now it has different parameters:

                                        // ok, it's missing the grabbing phase: check the collision:
                                        if (IsCollidingWithSomeItem(ItemIndex, GET.pItem->CordX, NewY, GET.pItem->CordZ, GET.pItem->Room,
                                                  3072, 250, 256, -128)==true) {
                                                  // found a collision: analyse what to do in according with object type
                                                   Flags = ActionForObject(ItemIndex, &ObjectTopY, VSpeed, true,true);
                                                   // restore GET.pItem because ActionForObject() function could have changed it
                                                   Get(enumGET.ITEM, ItemIndex,0);
                                        }

We set "true" for TestCrane, while we set "TestKill=true) only because crane is able to kill creature, in spite that argument it will be not used when we are working with the crane.


The changes in Crane_InputOthers() function

In this function we have to manage the command to do open the jaws (DRAW WEAPONS) in according with the CRANE_OCB_FALLING_DOWN_ITEM ocb setting and the CRANE_ITEM_ON_FLOOR flag:

          // check for "Open Jaws" command:
          // but only if there are one of two conditions:
          // there in ocb of crane the flag CRANE_OCB_FALLING_DOWN_ITEM (that allows to release item in any time
          // or
          // the grabbed item reached the floor and there is the CRANE_ITEM_ON_FLOOR flag
          TestCheckOpenJaws=false;
          if (GET.pItem->OcbCode & CRANE_OCB_FALLING_DOWN_ITEM) TestCheckOpenJaws=true;
          if (GET.pItem->Reserved_3A & CRANE_ITEM_ON_FLOOR) TestCheckOpenJaws=true;

          if (TestCheckOpenJaws == true && (FlagsInput & enumCMD.DRAW_WEAPON)!=0) {
                    // there is command to open jaws
                    // Change animation only if jaws was in grabbing position

To handle better this complicate condition we used a new local variable:

          bool TestCheckOpenJaws;

To remember if we have or less to check for DRAW WEAPONS command.

If the command is present we have to release the item, with a different behaviour depending on ocb setting:

                    // if crane was grabbing an item: manage the falling down of the grabbed item
                    if (IndexGrabbed != -1) {
                              if (GET.pItem->OcbCode & CRANE_OCB_FALLING_DOWN_ITEM) {
                                        // to do falling down the item
                                        CreateFallingDownForItem(IndexGrabbed, ItemIndex, 32, 16, 200, -1,
                                                                                          FDI_EARTH_QUAKE | FDI_ENGAGE_ROLLINGBALL | FDI_KILL_ENEMY);
                              }
                              // remove data for grabbed item
                              IndexGrabbed=-1;
                              GET.pItem->Reserved_38 = -1;
                    }

We call our function to create the progressive action to do falling down the item, but only when there was the CRANE_OCB_FALLING_DOWN_ITEM ocb to allow it.
We see also some FDI flag to customize the falling down.
Now we can testing the new code


Testing the code for falling down of the grabbed Item



The jeep falls down enough fine.
About speed and acceleration we can change those values if we wish.
Now we can try what happens when we let falling down the rolling ball over that slope.
Since we have set the FDI_ENGAGE_ROLLINGBALL flag, it should be engaged and continue to roll for the slope.


Ok, in this image is not very clear, but it works.
It rolls below for the slope, like it has been engaged with a trigger.

What yet it doesn't work



Not all is working fine...
If you see above images you understand the problem.
The jeep falls in the water but there is no splash, and more, the falling speed is very high also when the jeep is moving in the water: not very realistic.
So we have to perform some check about the presence of water in the falling path.


Adding splash and ripples on the water surface

We can add a splash on the water surface using the Splash() tomb raider function that requires as only ony argument the item structure of moveable that affected the splash.
To do ripples on the water, we use the other tomb raider function SetupRipple() that has following prototype:

SetupRipple(DWORD CordX, int CordY, DWORD CordZ, int Intensity, int Quantity);

Really I don't know so well the meaning of Intensity and Quantity, anyway I suppose that Intensity should be the size while quantity the number of circles.

We add to the code of AXN_FALLING_DOWN_ITEM action this code:


                    // Update position of item
                    GET.pItem->CordY = NewY;
                    UpdateItemRoom(ItemIndex);
                    CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
                    GET.pItem->HeightFloor = FLOOR.FloorHeight;
                    // check for water surface
                    if (FLOOR.WaterHeight != WALL_FLOOR && NewY > FLOOR.WaterHeight) {
                              // the item is inside of water room
                              // if item is yet closed to the surface to do ripples and splash
                              DistanceY = NewY - FLOOR.WaterHeight;

                              if (DistanceY < 200) {
                                        // item is yet very closed to the water surface
                                        // to do ripple and splash
                                        GetItemSize(ItemIndex, &TopY, &Radius);

                                        // if there is an high vertical speed to do also a splash
                                        if (pAction->VetArg[0] >= 64) {
                                                  
                                                  // to do splash
                                                  // verify if item is large or little
                                                  if (Radius < 200) {
                                                            // little splash
                                                            TriggerSmallSplash(GET.pItem->CordX, FLOOR.WaterHeight, GET.pItem->CordY, Radius*2);
                                                            SetupRipple(GET.pItem->CordX, FLOOR.WaterHeight, GET.pItem->CordZ, Radius * 2, 4);
                                                  }else {
                                                            // item is bigger: to do large splash
                                                            Splash(GET.pItem);
                                                  }

                                        }
                              }
                              // since item is in water, reduce its speed and max speed and clear accelleration
                              pAction->VetArg[0] = 16; // vertical speed
                              pAction->VetArg[1] = 0; // acceleration
                              pAction->VetArg[2] = 16; // max speed
                    }

After the (old) updating of item room, we check if there is a water surface in current room and position of the item.
If the value FLOOR.WaterHeight is even than WALL_FLOOR it means that there is no water surface below or over the item.
While if the value is different, that it the real Y coordinate of water surface and we check if Y coordinate of item is below of it:

                    if (FLOOR.WaterHeight != WALL_FLOOR && NewY > FLOOR.WaterHeight) {

When this condition it's true, we know that the item is in the water (or at least it begins to enter in the water)
To do the splash we check also if its Y coordinate is very closed to the water surface, because otherwise we'll do endlessly splash also when item is in depth water, underwater.

                              // the item is inside of water room
                              // if item is yet closed to the surface to do ripples and splash
                              DistanceY = NewY - FLOOR.WaterHeight;

                              if (DistanceY < 200) {

We'll perform changes on water surface only when the item is yet closed (200 game units) to water surface.
Then we choose if doing only some ripples or a larger splash.
To decide we check the current size:

                                        // item is yet very closed to the water surface
                                        // to do ripple and splash
                                        GetItemSize(ItemIndex, &TopY, &Radius);

                                        // if there is an high vertical speed to do also a splash
                                        if (pAction->VetArg[0] >= 64) {
                                                  
                                                  // to do splash
                                                  // verify if item is large or little
                                                  if (Radius < 200) {
                                                            // little splash
                                                            TriggerSmallSplash(GET.pItem->CordX, FLOOR.WaterHeight, GET.pItem->CordY, Radius*2);
                                                            SetupRipple(GET.pItem->CordX, FLOOR.WaterHeight, GET.pItem->CordZ, Radius * 2, 4);
                                                  }else {
                                                            // item is bigger: to do large splash
                                                            Splash(GET.pItem);
                                                  }

                                        }

Then, depending on its size, we'll perform one or other effect.

We force also the vertical speed to be and to remain very low, because the item now is sicking slowly in the water:

                              // since item is in water, reduce its speed and max speed and clear accelleration
                              pAction->VetArg[0] = 16; // vertical speed
                              pAction->VetArg[1] = 0; // acceleration
                              pAction->VetArg[2] = 16; // max speed

For this new code, we defined some new local variable:

          int DistanceY;
          int Radius;



Testing splash feature



It's not marvellous effect, anyway it works: there is a splash, some ripples and the jeep after water surface slow down sinking in the water.
Since we are working on this feature, we should add same effect also when it is the crane to across the water surface but only when it is moving fastly, in falling down mode, because when it move down slowly it's not useful a splash and neither reduce its speed, since it is already very slow. When the crane is moving slowly we'll add only little ripples.


Changes in Crane_MoveCraneVertically() function to do splash and ripples for the crane

When the crane is falling down fastly we'll add a Splash:

          // ------ Change vertical speed ----------------
          if (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN) {
                    // the crane should falling down
                    // we have to do gravity simulation: increase the Vertical speed until it reaches a max value
                    VSpeed += 8;
                    if (VSpeed > 128) VSpeed=128;

                    // but if the crane entered in water (with its top side) slow down it, and fix its speed at 16
                    if (FLOOR.WaterHeight != WALL_FLOOR) {
                              // there is water
                              // discover top y coordinate of the crane
                              Get(enumGET.ITEM_COLL_BOX, ItemIndex,0);

                              ObjectTopY = GET.pItem->CordY + GET.pCollItem->MinY;
                    
                              if (ObjectTopY > FLOOR.WaterHeight) {
                                        // force vertical speed at 16 (slow)
                                        VSpeed=16;
                              }
                              // if base Y of crane is very closed to water surface: to do splash
                              if (GET.pItem->CordY > FLOOR.WaterHeight) {
                                        IncY = GET.pItem->CordY - FLOOR.WaterHeight;

                                        if (IncY < 200) {
                                                  Splash(GET.pItem);
                                        }
                              }
                    }

In above code we reduced the vertical speed when the top side of the crane is below the water surface:

                              // discover top y coordinate of the crane
                              Get(enumGET.ITEM_COLL_BOX, ItemIndex,0);

                              ObjectTopY = GET.pItem->CordY + GET.pCollItem->MinY;
                    
                              if (ObjectTopY > FLOOR.WaterHeight) {
                                        // force vertical speed at 16 (slow)
                                        VSpeed=16;
                              }

Then we add a Splash() effect when the bottom side of the crane (identified by its CordY field) is very closed to water surface:

                              // if base Y of crane is very closed to water surface: to do splash
                              if (GET.pItem->CordY > FLOOR.WaterHeight) {
                                        IncY = GET.pItem->CordY - FLOOR.WaterHeight;

                                        if (IncY < 200) {
                                                  Splash(GET.pItem);
                                        }
                              }


Above was code for the crane was falling down, while when the crane is moving slowly, upstairs or downstairs, we add only little ripples on the water surface:

          // compute future Y coordinate:

          NewY= GET.pItem->CordY + VSpeed;

          // if crane is movign slowly and there is a water surface...
          if (FLOOR.WaterHeight != WALL_FLOOR &&
                    (VSpeed == -16 || VSpeed==16) &&
                    GET.pItem->CordY >= FLOOR.WaterHeight) {
                    // add ripple on water surface when crane is moving slowly
                    // if crane is acrossin the water surface to do little ripple
                    Get(enumGET.ITEM_COLL_BOX, ItemIndex, 0);
                    ObjectTopY = GET.pItem->CordY + GET.pCollItem->MinY;

                    // perform a ripple when top side of object is closed to water surface

                    if (AbsDiffY(ObjectTopY, FLOOR.WaterHeight) < 17) {
                              // top side of crane is close to the water surface.

                              SetupRipple(GET.pItem->CordX, FLOOR.WaterHeight, GET.pItem->CordZ, 1000, 4);
                              
                    }

                    if (AbsDiffY(GET.pItem->CordY, FLOOR.WaterHeight) < 300) {
                              // bottom side of the crane is closed to the water surface
                              // if crane has open jaws: to do ripples where there are four jaws
                              // while if jaws are closed, to do only one ripple at center
                              if (GetRelativeAnimation(GET.pItem)==0) {
                                        // closed jaws
                                        SetupRipple(GET.pItem->CordX, FLOOR.WaterHeight, GET.pItem->CordZ, 128, 4);
                              }else {
                                        // to do four ripples on four corners on current sector where (about) there are the four teeth
                                        // of the crane
                                        SetupRipple(GET.pItem->CordX+480, FLOOR.WaterHeight, GET.pItem->CordZ+480, 128, 4);
                                        SetupRipple(GET.pItem->CordX+480, FLOOR.WaterHeight, GET.pItem->CordZ-480, 128, 4);
                                        SetupRipple(GET.pItem->CordX-480, FLOOR.WaterHeight, GET.pItem->CordZ+480, 128, 4);
                                        SetupRipple(GET.pItem->CordX-480, FLOOR.WaterHeight, GET.pItem->CordZ-480, 128, 4);
                              }
                    }
          }

In above code did difference if crane is moving up and down, if it is has open jaws or less, to detect the precise point where to add the ripple effect.
When the crane is moving down with open jaws, we'll add four little ripples, one for each tooth of the jaws:

                                        // to do four ripples on four corners on current sector where (about) there are the four teeth
                                        // of the crane
                                        SetupRipple(GET.pItem->CordX+480, FLOOR.WaterHeight, GET.pItem->CordZ+480, 128, 4);
                                        SetupRipple(GET.pItem->CordX+480, FLOOR.WaterHeight, GET.pItem->CordZ-480, 128, 4);
                                        SetupRipple(GET.pItem->CordX-480, FLOOR.WaterHeight, GET.pItem->CordZ+480, 128, 4);
                                        SetupRipple(GET.pItem->CordX-480, FLOOR.WaterHeight, GET.pItem->CordZ-480, 128, 4);

While when the jaws are closed, and we discover this by animation 0, we add only one ripple at center:

                              if (GetRelativeAnimation(GET.pItem)==0) {
                                        // closed jaws
                                        SetupRipple(GET.pItem->CordX, FLOOR.WaterHeight, GET.pItem->CordZ, 128, 4);
                              }else {


Now we can test this new code...

Testing Splash and Ripples for the Crane



The splash works, now we see the ripples, since here it's important the position of them, entering and coming out from the water:


Ok, they are about in right position.

Other fixing for the crane

Performing many tests we discovered many little problems in our crane code.
The first happens at begin of the whole management: when lara engages the crane from the control panel, her position respect to the control panel, is sometimes good, other times is less good.


At left we see ideal position of lara, with her right hand on the joystick, while in right image lara is too moved at left and her hand is very far by joystick.
It's true that just a moment later, the crane camera will begin and lara will be no more visible but anyway it's better understanding how to fix this problem because in other situations, like when lara has to align with real vehicles, it could be a big problem having a misalignment between lara and the vehicle.
To align lara to control panel, reaching the ideal position we'll use the AlignLaraAtPosition() function

The AlignLaraAtPosition() function

bool AlignLaraAtPosition(StrTestPositionCmd *pTestPosition, int ObjectIndex)

This function is very alike than CheckPositionAlignment() function, as arguments and target, but there is a big difference: while CheckPositionAlignment() performs only a check about position of lara respect the given item, the AlignLaraAtPosition() moves really Lara to put her in ideal position respect the item

The AlignLaraAtPosition() function returns "true" or "false".
When it returns "true" it means that alignment has been completed and lara reached ideal position. This will be the moment to engage our animation.
While when it returns false, it means that the function is yet moving lara.
This means that will have to call many times this function until it returned "true".
While when it returns "false" we'll quit the code.

It's important understanding that once you called first time the AlignLaraAtPosition() function, until you get a "true" value returned by it, your code will enter in a Alignment Loop, this means that your code should only verify if the alignment is yet in progress and if it is, call newly the AlignLaraAtPosition() function.
To detect if there is a alignment loop in progress you can check two global variables:

Trng.pGlobTomb4->TestAlignmentInProgress
Trng.pGlobTomb4->pAdr->pObjectActive

Note: since pObjectActive variable is a pointer to a single value, you should read its value with the syntax:

if (*Trng.pGlobTomb4->pAdr->pObjectActive == ItemIndex) {

TestAlignmentInProgress is "true" when there is an alingment in progress, but then you should check also the index of object whom lara is trying to align with it, because it could be also an alignment that it's happening with other items.
Now we can see how to change the code in CollisionCtrlPanelCrane() function to align lara at ideal position

Changes in CollisionCtrlPanelCrane() function to support Lara's alingment


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

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

          // if there is alignment in progress on current control pannel: call immediately AlignLaraAtPosition()
          if (Trng.pGlobTomb4->TestAlignmentInProgress == true && *Trng.pGlobTomb4->pAdr->pObjectActive == ItemIndex) {
                    if (AlignLaraAtPosition(&CtrlCraneTestPosition, ItemIndex) == false) return;
                    // lara is aligned..
                    // engage the driving of the crane
                    Crane_EngageDriving(ItemIndex);
                    return;
          }
          Get(enumGET.INPUT,0,0);
          if (GET.Input.GameCommandsRead & enumCMD.ACTION) {
                    // it has been sent ACTION command
                    if (pLara->StateIdCurrent == 2 && pLara->AnimationNow == 103) {
                              // lara is in stand-up position (stateid=2 animation=103)
                              Get(enumGET.INFO_LARA,0,0);
                              if (GET.LaraInfo.TestFreeHands == true) {
                                        // lara has free hands
                                        if (CheckPositionAlignment(&CtrlCraneTestPosition, ItemIndex)== true) {
                                                  // lara is about in front of the control panel
                                                  // perform first call to AlignLaraAtPosition() function:
                                                  if (AlignLaraAtPosition(&CtrlCraneTestPosition, ItemIndex) == false) return;
                                                  // lara is aligned..
                                                  // engage the driving of the crane
                                                  Crane_EngageDriving(ItemIndex);
                                                  return;                    
                                        }
                              }
                    }
          }

Now we can test the alignment in game...

Testing self-alignment of Lara with Control Panel



Ok, it works fine.
In spite (picture at left) lara was not in better position, the engine moved her in ideal position, so when the code performed the engaging animation lara was in ideal position.
You can see in picture at right, that now the hand of lara is perfectly over the joystick.


Failure of AlignLaraAtPosition() function

In some circustances you could have the feeling that AlignLaraAtPosition() doesn't work fine.
It happens when you set the right values you got from log of LogItem= script command, but when you try to get the self-alignment of lara, she moved in wrong direction, very often, in endless way..
When you have this problem the solution is to try to set some TPOS_ flag to fix the original facing of the item.
Currently there are two flags to try:

#define TPOS_OPPOSITE_FACING 0x0010
#define TPOS_TURN_FACING_90 0x0040

You can read the descriptions also in NG_Center, anyway these flags try to convert the facing of item, and the corresponding difference on Z and X axis, like the item was rotate of 180 degrees ( TPOS_OPPOSITE_FACING) or by 90 degrees (TPOS_TURN_FACING_90).
Really I've not yet discovered the reason of this problem, but the tomb raider function that perform this self-adjustmen, seems used some internal data about the item that are not visible by animation editor.
Ayway only way to know if you need of some one of these flags is to try...
Try with no corrector flags, and you see if it works fine.
If it doesn't, try with one of them, and then with other.
Probably you could also using both flags in same moment, anyway one of them should fix the problem.
With our alignment on control panel I had same problem at start, and indeed I had need to use the TPOS_TURN_FACING_90 flag to fix that problem.
In the code of InitializeAll() function, when we initialise data to use for testposition, we type own this flag in our testpositioncmd structure:

          CtrlCraneTestPosition.Flags = TPOS_TURN_FACING_90; // TPOS_ flags


To set a vertical limit for Crane

There is another problem to fix.
Try to do this experiment:
- Engage the crane
- Move it over the top-left sector of swimming pool.
- Let fall down the crane
- Now hit ROLL (end) to quit the driving of the crane
- And now look fine the crane...


You see (above in right picture) that the pole of the crane is not enough long to cover all distance until the ceiling.
Ok, we could change that mesh to increase its length, but the problem will exist always: we cann't having an unlimited length for that pole.
By other hand it's not either realistic having a too long pole/rope.
More, some level designer could need of a limited lenght of the crane to place some moveable item with the assurance that the crane (and the player) will be not able to move them, since with some item there could be troubles to move in other side.
If we had a limit about vertical movement of the crane, we could plan our level placing some (to do not move) items over the crane limit, while others, that we allow to move them, a bit upper to be reached by this limit.

How to handle a numeric value in ocb field

As usual, when you mean set limit or chance it's better letting to level builder the opportunity to customize these settings.
So we can use the ocb field of crane to set also max vertical height that the crane will be able to reach.
We can set this value in number of sectors, so also with a limited number we can cover wide distance.
Anyway now we have a problem: we have already used the ocb field to host some ocb flags, so now we cann't cover them with another value for vertical limit.
The trick to solve this problem is to assign only some bits (that is equivalent of flags) to assign the vertical limit, preserving other flags, already assigned and some other also for future flags.
To understand better this compute it will be useful have some notion about binay system.
One bit may be 0 or 1, so we can host two values, like a flag where it has two values: the flag is present "1", or it is missing "0"
When we use two bits or another group of bit the formula to discover how big value we can host is this:

NumberOfValues = NumberOfBits ^ 2

Where the "^" means: power by 2.
For instance:
Using 2 bits we have:

NumberOfValues = 2 ^ 2 = 4

With 4 bits we have:

NumberOfValues = 4 ^ 2= 16

Above computations is necessary to understand how much bits we have to assign to host a reasonalble max value for vertical limit.
So, we can choose to assign 5 bits, since it means 2> until to 32 sectors, a huge height.
Now we see the ocb flags (and bits) we have already assigned:

// Ocb flags for swinging crane
#define CRANE_OCB_AUTOMATIC 0x0001
#define CRANE_OCB_MANUAL 0x0002
#define CRANE_OCB_SHADOW 0x0004
#define CRANE_OCB_PROP_SHADOW 0x0008
#define CRANE_OCB_HARD_GRABBING 0x0010
#define CRANE_OCB_FALLING_DOWN_ITEM 0x0020

We typed in hexadecimal format (with "0x" prefix) because it's more easy follow the bit position.
We can set to assign for vertical limit the five bits:

0x0100
0x0200
0x0400
0x0800
0x1000

In this way we have not overlapped other already assigned flags.
Since we place first bit (0x0100) in third hecadecimal digit, this means that this value has decimal value 256, since 0x100 = 256.
This means that the formula to type the max limit for vertical height, it will be:

OcbFlags + VerticalLimit*256

Indeed, multiplying by 256 is like we moved the number of 8 binary digits at left.

Using a bit mask to work only on some bit group

Once we discovered how to set in ocb field the value for vertical limit, now we have to find a way to extract that value from ocb field, performing opposite computation.
Pratically we should divide by 256 the ocb value but... there are yet other bits in ocb field and in this case result could change.
To avoid this problem we'll mask the value in ocb field to see only the bit we are interested.
The mask will be the sum of all bits where we hosted the vertical limit:

0x0100
0x0200
0x0400
0x0800
0x1000
----------
0x1F00 // mask for all above bits

If you check, you see that the sum of all those bits will give own "0x1f00".
Once we assigned (to get easier, as usuale, the usage of constant values) a mnemonic constant:

#define CRANE_OCB_MAKS_VERTICAL_LIMIT 0x1F00

Then we can get the original value of vertical limit with this computation:

          Temp = OcbValue & CRANE_OCB_MAKS_VERTICAL_LIMIT;

          Temp = Temp / 256;

Where, in first instruction we cleared all bits different by those of our mask.
Then we divided by 256 that cleaned value.

Why to do binary shift?

Above code should work, anyway do you remember when I said that muliply by 256 it means move a value of 8 binary digits at left, or that, dividing by 256 it means move a value by 8 binary digits at right?
Well, in C++ langugage (but also in assembly) there is an instruction very faster than divide and mutiply, to move a value of N digits at left or at right.
Since it is faster it's better using this instruction.
In C++ language we can use the "<<" operator to multiply by N a value. Or the other opposite operator ">>" to divide by N a value.
This N has to be some power by 2 value, like: 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048 ect.
This operation is a binary shift where you move the position of all bits of that value.
When you wish multiply by 4 a value, you type:

Result= Value << 2;

Why do we use "2" as N?
Because moving by 2 position a value in binary, it means multiply it by 4, since 2> is 4.
To multiply by 256, since to get the power by 2 value of 256 we had to do: 2^8, we'll use "8" as N:

Result= Value << 8;

While to divide a number by 256, we'll use always 8 but other operator:

Result = Value >> 8;


The GetCraneOcbVerticalLimit() function

To have always easily available the value of vertical limit, we can type the code to extract that value in a little function.
This function will have as input argument the ocb value of crane:

int GetCraneOcbVerticalLimit(WORD OcbValue)
{
          WORD Temp;

          Temp = OcbValue & CRANE_OCB_MAKS_VERTICAL_LIMIT;
          // divide by 256
          Temp = Temp >> 8;
          if (Temp == 0) {
                    // no limit it has been set: used default limit of 5 sectors
                    Temp=5;
          }
          // multiply by 1024 (one sector = 1024 game units)
          Temp = Temp << 10;
          return Temp;
}

We used the code that we have already explained, anyway we added also a condition about the limit in ocb was 0.
In this case it means that level builder didn't set any value but we cann't having as limit 0, so we use a default value by 5 sectors.
In this way the setting of vertical is optional: if level builder is not interested it can omit to set it.
The function will return the value in game units, and since one sector means 1024 game units, it will multiply by 1024 the number of sectors.
We used the usual trick of shift to multiply: we used "10" because:

2 ^ 10 = 1024.


How to manage the vertical limit for the crane

Now we should write the code to handle this vertical limit.
When the crane is moving down, or falling down, we'll have to check not only for floor or collision with other items, but also we should verify if the crane reached its max distance from ceiling. In that case we'll stop it like it hits the floor.

Changes in Crane_MoveCraneVertically() function to support max vertical limit

At begin of function we compute the Y coordinate corresponding to max vertical distance from ceiling, and store this value in a new local variable: YVerticalLimit

          int YVerticalLimit;

          VSpeed = GET.pItem->Reserved_36;
          IndexGrabbed= GET.pItem->Reserved_38;
          // if there was the CRANE_GRABBING_IN_PROGRESS flag, it is like the grabbed item was not present
          if (GET.pItem->Reserved_3A & CRANE_GRABBING_IN_PROGRESS) {
                    IndexGrabbed=-1;
          }

          // discover the Y limit about max vertical distance reached by crane
          YVerticalLimit= GetCraneOcbVerticalLimit(GET.pItem->OcbCode);
          // now we add to this distance the origin: the ceiling Y coordinate
          YVerticalLimit += FLOOR.CeilingHeight;


Then we have to add code in two situations where we detect collision moving down the crane: when the crane is grabbing an item, and when the crane is empty.
While the crane is grabbing an item, and it is moving down, we check after all controls.
When it seems there is no collision, we have to do a further control to see that crane didn't reach max vertical distance from floor:

                              // if there is collision: stop here
                              if (TestCollision) {
                                        // restore old Y coordinate of crane
                                        NewY= GET.pItem->CordY;
                                        // and clear vertical speed
                                        VSpeed=0;
                                        // inform that item reached the floor
                                        GET.pItem->Reserved_3A |= CRANE_ITEM_ON_FLOOR;
                              }
                              // no collision with floor and items, but check if crane reached max vertical limit
                              if (TestCollision==false) {
                                        if (NewY >= YVerticalLimit) {
                                                  // we reached the limit: stop speed
                                                  VSpeed=0;
                                                  NewY= YVerticalLimit;
                                        }
                              }


And now we check when the crane was empty:

                              // check if crane reached the floor
                              if (NewY >= FloorY) {
                                        // yes: reached the floor: stop the movement
                                        VSpeed=0;
                                        NewY = FloorY;
                                        // effects to have touched the floor
                                        ForceAnimationForItem(GET.pItem, 2, 0);
                                        SoundEffect(GetSFX(SFX_CRANE_TOUCH_FLOOR, 314), &GET.pItem->CordX, 0);
                                        if (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN) {
                                                  // to do earthquake only if there was falling down
                                                  // earth quake: flipeffect 1
                                                  PerformFlipeffect(NULL, 1, 0, 0);

                                                  // remove the CRANE_FALLING_DOWN flag
                                                  GET.pItem->Reserved_3A &= ~CRANE_FALLING_DOWN;
                                        }          
                              }else {
                                        // crane not reached the floor, anyway verify if it reaches max vertical distance:
                                        if (NewY >= YVerticalLimit) {
                                                  // yes: stop it
                                                  VSpeed=0;
                                                  NewY = YVerticalLimit;
                                                  // remove (further) CRANE_FALLING_DOWN flag
                                                  GET.pItem->Reserved_3A &= ~CRANE_FALLING_DOWN;
                                        }
                              }

Theoratically these changes should be enough to handle max vertical limit, anyway performing some test I discovered a bug, indirectly linked with this new feature.
If while the crane is moving down and grabbing an item, it will be stopped for vertical limit, the vertical speed will become 0, but the grabbing is yet in progress.
In the Crane_UpdateAnimations() function, it has been an error, to check animations about grabbing only when the speed is positive (crane is moving down) because the grabbing in progress we should handle it, with any speed, and also missing of vertical speed.
For this reason we have to change a bit also the Crane_UpdateAnimations() function.

The new Crane_UpdateAnimations() function

While in old code we check for current animation in according with direction of vertical speed, omitting all controls when the vertical speed was 0, now we perform a distinct check for animation, ignoring the vertical speed, only to manage further animation in grabbing phase.

void Crane_UpdateAnimations(short ItemIndex)
{
          int NumAnim;

          NumAnim = GetRelativeAnimation(GET.pItem);

          // handle grabbing phases and flags:
          switch (NumAnim) {
                    case 6:
                              // crane has large grabbing
                              if (GET.pItem->Reserved_36 < 0) {
                                        // crane is moving up!
                                        // if there is no grabbed item or grabbing in progress: to do close newly the jaws
                                        if ((GET.pItem->Reserved_3A & CRANE_GRABBING_IN_PROGRESS)==0 &&
                                                  GET.pItem->Reserved_38 == -1) {
                                                  ForceAnimationForItem(GET.pItem, 10, 0);
                                                  break;
                                        }
                              }
                              /// if required slim grabbing: set animation for slim grabbing
                              if (GET.pItem->Reserved_3A & CRANE_REQUIRES_SLIM_GRAB) {
                                        ForceAnimationForItem(GET.pItem, 7, 3);
                                        // now remove the flag to avoid mess in future with other attempts of grabbing
                                        GET.pItem->Reserved_3A &= ~CRANE_REQUIRES_SLIM_GRAB;
                              }
                              // now the code will continue with "case 8:" (no break;)
                    case 8:
                              // crane has fixed grabbing animation (slim or large)
                              // if there was the CRANE_GRABBING_IN_PROGRESS flag: remove it, and stop the vertical movements
                              if (GET.pItem->Reserved_3A & CRANE_GRABBING_IN_PROGRESS) {

                                        GET.pItem->Reserved_3A &= ~CRANE_GRABBING_IN_PROGRESS;

                                        GET.pItem->Reserved_36 =0;
                              }
                              break;
          }
          // handle animations in according with further direction on vertical movments
          if (GET.pItem->Reserved_36 > 0) {
                    // crane is moving down: open the jaws
                    switch (NumAnim) {
                    case 5:
                              // jaws already open: nothing to do
                              break;
                    case 0:
                              // crane has close jaws: set animations to do open them
                              ForceAnimationForItem(GET.pItem, 1, 1);
                              break;
                    default:
                              // all other cases: don't change anything while an animation of change jaws is in progress
                              break;
                    }
          }
          if (GET.pItem->Reserved_36 < 0) {
                    // crane is moving up: close the jaws
                    // at least it was not grabbing an item or the jaws are already close

                    switch(NumAnim)
                    {
                    case 0:
                              // crane is already close: ok, nothing to do
                              break;
                    case 5:
                              // crane is open: close the jaws
                              ForceAnimationForItem(GET.pItem, 2, 0);
                              break;
                    default:
                              // crane is changing its jaws : do not change anything now to don't break an animation in the middle
                              break;
                    }
          }
}


Testing new vertical limit

Before testing the vertical limit we have to set a vertical limit...
So open with NGLE the plugin project, move to room 23, click on crane item and type O to show the object code panel.
Now type as ocb code: 1570
We get this value applying following flags:

#define CRANE_OCB_MANUAL 0x0002
#define CRANE_OCB_FALLING_DOWN_ITEM 0x0020
+ 6 sectors as vertical limit, i.e. 6*256+2+32= 1570.

If we see these numbers in hexadecimal format is more easy understanding the composition of final ocb:

0x0002 CRANE_OCB_MANUAL
0x0020 CRANE_OCB_FALLING_DOWN_ITEM
0x0600 1536 (6*256) sectors as max vertical limit
----------
0x0622 = decimal 1570

So now we build the level, supposing to have already updated the library, and to see if the limit of 6 sectors now works...


Well, we see that now the crane it will be stopped before touching the bottom of swimming pool, because it reaches 6 sectors of vertical limit (picture at left)
As (good) collateral effect we see (in right image) that now the pole reaches the ceiling.

Updating also automatic crane with vertical limit

Since the ocb setting about vertical limit it could be interesting also for automatic crane we saw in previous exercise, we should change also that code to support the vertical limit.
In this case it is easier.
The function used by automatic crane for vertical movement was the Crane_MoveUpDown() function:

void Crane_MoveUpDown(short ItemIndex)
{
          int VSpeed;
          int MinBaseCeiling;
          int YVerticalLimit;

          VSpeed= GET.pItem->Reserved_36;

          if (VSpeed > 0) {
                    // vertical speed is positive
                    // the crane is falling down.
                    // we have to do gravity simulation: increase the Vertical speed until it reaches a max value
                    VSpeed += 8;
                    if (VSpeed > 128) VSpeed=128;

                    GET.pItem->CordY += VSpeed;
                    
                    // we have to verify to have not reached or passed over the current floor height

                    if (GET.pItem->CordY > GET.pItem->HeightFloor) {
                              // forbid to enter inside of the floor
                              GET.pItem->CordY = GET.pItem->HeightFloor;
                              // since we reached the floor, now we have to do come back the crane
                              // setting a negative vertical speed
                              VSpeed = -32;
                              ForceAnimationForItem(GET.pItem, 2, -1);
                              SoundEffect(GetSFX(SFX_CRANE_TOUCH_FLOOR, 314), &GET.pItem->CordX, 0);
                              // earth quake: flipeffect 1
                              PerformFlipeffect(NULL, 1, 0, 0);

                    }else {
                              // crane didn't reach the floor but now we verify if it reached max vertical limit:
                              // discover the Y limit about max vertical distance reached by crane
                              YVerticalLimit= GetCraneOcbVerticalLimit(GET.pItem->OcbCode);
                              // now we add to this distance the origin: the ceiling Y coordinate
                              YVerticalLimit += FLOOR.CeilingHeight;

                              if (GET.pItem->CordY >= YVerticalLimit) {
                                        // crane reached max limit: stop it
                                        GET.pItem->CordY = YVerticalLimit;
                                        // setting a negative vertical speed
                                        VSpeed = -32;
                                        ForceAnimationForItem(GET.pItem, 2, -1);
                                        SoundEffect(GetSFX(SFX_CRANE_TOUCH_FLOOR, 314), &GET.pItem->CordX, 0);
                              }
                    }

This if only first side of the function but all required code is present.
When there is no collision with floor, we check if crane reached the max vertical limit.
When it happens we stop the crane at same Y coordinate of the limit, and then we set a negative vertical speed to do move up the crane, like it happens always with automatic crane.
Now we build the library but before performing a test in game, we shoudl change the ocb value of crane in room 18 (that of automatic crane) and setting a vertical limit very low, otherwise we'll be not able to verify if it is working. So we set only 3 sectors as max vertical limit and then we add also "1" to enable automatic crane.
So the formula will be: 1 + 3*256= 769
Or, computing with other hexadecimal system, it will be:

0x0001 CRANE_OCB_AUTOMATIC
0x0300 3 sectors
----------
0x301 in decimal is 769

Type 769 as ocb code, build the level and choose "Exercise 5: The Swinging Crane" level
We can let Lara below the crane because (if the limit will work) the crane will be not able to reach the head of Lara.

Testing max vertical limit on Automatic Crane



Ok it works fine. The crane stops before reaching the head of Lara (left image) and then it comes back upward (right image)

Another bug to fix about automatic grabbing

Keeping current ocb value in the manual crane, the value 1570 to set:

0x0002 CRANE_OCB_MANUAL
0x0020 CRANE_OCB_FALLING_DOWN_ITEM
0x0600 1536 (6*256) sectors as max vertical limit

I discovered another bug.
It happens when crane is grabbing an item, the crane is moving down and we give the command to open jaws:


In A picture you see just a moment later that crane released the item. In this moment, both items are moving down: the crane for its movement, and the jeep because it begun the falling down with progressive action.
Apparently, in B picture all is right: the jeep reached the floor, but there is a weird problem: the crane is not able to move down in spite there is yet many space below it.
In C picture is clear the problem: when we give the command to move up the crane, also the jeep is moving up,
In D picture we have the confirm that jeep is following the crane movements: when we give the command to move the crane at right also jeep is moving at right.
It was like the jeep was yet grabbed by the crane...
But what happened, why?
I believe that all problems are showed in above A picture...
Since the crane is moving down while it is grabbing the item, and it will continue to move down once let falling down the item, it happens this:
  1. The crane is moving down while is grabbing the jeep
  2. There is command to open jaws and let falling down the jeep
  3. The jeep will be handled by progressive action to fall down
  4. The crane now is empty
  5. The code now checks if crane is able to grab some item, since it is moving down
  6. The code finds owns the jeep as item to grab, because it elapsed only one frame and the jeep is very closed to the crane
  7. The code assigns the jeep as grabbed by the crane
  8. But the progressive action is moving down the jeep
  9. Once the progressive action for falling down it has been completed the crane is able to move up the jeep because it's seeen as "grabbed"

  10. Same speech for horizontal movements: the jeep will be moved with the crane at right

Pratically the crane leaves and then takes newly the grabbed iem only becase that item is very closed to its jaws.
To avoid this problem we should find a way to set a sleep time between "releasing item" and "try to grab another item" phases, otherwise it will be grabbed the item just left.
We can put some delay operations in the middle:
  1. Once we receive the command to open jaws, if the crane was grabbing an item, we stop the vertical movement of the crane. In this way, at next frame it will be not able to detect items to grab, because that happens only when it is moving down, while now we stopped it.

  2. Also in automatic grabbing performing a condition about current animation: the crane should have open jaws to begin the grabbing, otherwise the grab will fail
    Since when the crane was grabbing it had grabbed jaws, to get newly open, it will require some time and while time the jaws don't come back open, it will be not possible grabbing another item.


Changes in Crane_MoveCraneVertically() function to fix automatic grabbing bug


                              case AFO_GRAB_LARGE:
                              case AFO_GRAB_SLIM:
                                        // if there is hard grabbing, verify if there is right animation in progress
                                        if (GET.pItem->OcbCode & CRANE_OCB_HARD_GRABBING) {
                                                  if (GetRelativeAnimation(GET.pItem) == 3) {
                                                            // animation is right
                                                            GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                            IndexGrabbed= COLLIDE.ItemIndex;
                                                            SignalMovedItem(IndexGrabbed);
                                                            // now we have to rememebr that the grabbing phase begun:
                                                            GET.pItem->Reserved_3A |= CRANE_GRABBING_IN_PROGRESS;
                                                            // verify if it requires slim grabbing
                                                            if (Flags == AFO_GRAB_SLIM) {
                                                                      // yes, set flag to remember that requires slim grabbing
                                                                      GET.pItem->Reserved_3A |= CRANE_REQUIRES_SLIM_GRAB;          
                                                            }
                                                            // set that currently the grabbed item is yet on the floor
                                                            GET.pItem->Reserved_3A |= CRANE_ITEM_ON_FLOOR;
                                                  }else {
                                                            // animation is wrong: to do fail the grabbing
                                                            // consider item only as floor collision
                                                            FloorY=ObjectTopY;
                                                  }
                                                  break;
                                        }
                                        // automatic grabbing mode (no CRANE_OCB_HARD_GRABBING flag)...
                                        // to accept the grabbing, it's necessary that crane had open japws animation:
                                        if (GetRelativeAnimation(GET.pItem) == 5) {
                                                  // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                                  GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                  IndexGrabbed= COLLIDE.ItemIndex;
                                                  SignalMovedItem(IndexGrabbed);
                                                  // set animation of grabbing
                                                  if (Flags == AFO_GRAB_LARGE) {
                                                            // from open to large grabbing
                                                            ForceAnimationForItem(GET.pItem, 3, 2);
                                                  }
                                                  if (Flags == AFO_GRAB_SLIM) {
                                                            // from open to slim grabbing
                                                            ForceAnimationForItem(GET.pItem, 12, 3);
                                                  }
                                                  // now we have to rememebr that the grabbing phase begun:
                                                  GET.pItem->Reserved_3A |= CRANE_GRABBING_IN_PROGRESS;
                                                  // set that currently the grabbed item is yet on the floor
                                                  GET.pItem->Reserved_3A |= CRANE_ITEM_ON_FLOOR;
                                        }else {
                                                  // current animation is not valid for grabbing:
                                                  // stop crane and force close jaws
                                                  // use as floory the top Y coordinate of object
                                                  FloorY = ObjectTopY;                                                  
                                        }
                              }

When there is no CRANE_OCB_HARD_GRABBING, and we managed the chance to perform automatic grabbing, we verify also that current animation was the number 5, that with open jaws. If it's true we enable the grabbing:

                                        // automatic grabbing mode (no CRANE_OCB_HARD_GRABBING flag)...
                                        // to accept the grabbing, it's necessary that crane had open japws animation:
                                        if (GetRelativeAnimation(GET.pItem) == 5) {
                                                  // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                                  GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                  IndexGrabbed= COLLIDE.ItemIndex;
                                                  SignalMovedItem(IndexGrabbed);

But when the animation is different than number 5, we'll refuse the grabbing and consider the item like a collision:

                                        }else {
                                                  // current animation is not valid for grabbing:
                                                  // stop crane and force close jaws
                                                  // use as floory the top Y coordinate of object
                                                  FloorY = ObjectTopY;                                                  
                                        }


Changes in Crane_InputOthers() function to fix automatic grabbing bug

When we received the command to open jaws, while there is a grabbed item, we have to stop the vertical movement of the crane:

                    // if crane was grabbing an item: manage the falling down of the grabbed item
                    if (IndexGrabbed != -1) {
                              if (GET.pItem->OcbCode & CRANE_OCB_FALLING_DOWN_ITEM) {
                                        // to do falling down the item
                                        CreateFallingDownForItem(IndexGrabbed, ItemIndex, 32, 16, 200, -1,
                                                                                          FDI_EARTH_QUAKE | FDI_ENGAGE_ROLLINGBALL | FDI_KILL_ENEMY);
                                        // stop vertical movement of the crane
                                        GET.pItem->Reserved_36 = 0;
                              }
                              // remove data for grabbed item
                              IndexGrabbed=-1;
                              GET.pItem->Reserved_38 = -1;
                    }          

Now we can build the library and verifing the fixing...

Testing the fixing for automatic grabbing



Ok, the code is working like we wished.
Once we open jaws, the crane stops its vertical movement. Once the jeep touch the floor we can verify that the crane is not grabbing the jeep, indeed we can move the crane while the jeep is still on the floor.

Houston, we've had a problem

Oh yeah a big problem with pushable object...
When we try to grab the pushable item in the swimming-pool, we fail, also using the (current setting in crane ocb) automatic grabbing.
The reason is that this pushable has floor collisions to be walkable by Lara and for this reason, when the crane is moving down, it touches the floor collision "sinked" in the pushable and the code stops the operation, like if crane collided really with the floor.

There is a way to solve this problem, and it is the same way used when lara has to push/pull a pushable item of this kind.
Just removing the floor collisions inside from the pushable, just a moment before moving it, and then, when the moving it has been completed, restore them.

The StartMoveItem() and EndMoveItem() functions

To remove the floor collisions from a special pushable object we can use the StartMoveItem() function.

bool StartMoveItem(int ItemIndex)

The name is own to remember that, everytime you move with your code a moveable, you should call this function, passing as argument the index of moveable you are going to move.
If the item is a pushable object with special collisions, this function will remove them and restore "flat" floor under the item, while it it's not a special pushable, the item will be ignored.
The twin function of StartMoveItem() is the EndMoveItem() function.

void EndMoveItem(int ItemIndex)

It has same argument, the index of item, but you should call it only when you completed the moving of item. This function, if the item was a special pushable item, will restore the floor collisions to get that pushable was walkable.

My idea to solve the problem was this:
  1. Increase the collision box of pushable item as height to get that it collided with crane first of inside special floor collisions.
  2. When the code detects the first collision with pushable, we call the StartMoveItem() function with pushable index, to remove the special floor collision.
  3. Now pushable item became a common moveable, and the crane will be able to move down to grab it
  4. When the crane, after moving, will leave it on the floor, we'll call the EndMoveItem() function to restore its special collisions

It should work, but it doesn't

The collision box of the Crane

I had changed the collision box of pushable item in this way:


You see at top, where I added a little space?
It was necessary to give the chance to collision box of crane to collide with collision box of the pushable in advance, respect to the floor collisions inside of the pushable.
But when I saw the collision box of the crane, in animation used to start the grabbing when it is yet fully open, I understood the problem:


The collision box seems ok, but do you see that space between the collision box (bottom side) and the floor?
The Y coordinate of the crane is always on the floor, so when there is this animation, the crane has its meshes (and its collision box) suspended over the floor about one click (a bit more, really).
Since in the Crane_MoveCraneVertically() function, we used the Y crane coordinate to detect the collision with the floor, the crane will detect the floor before its collision box collided with that of pushable item.
We can explain better this situation using a picture


The red points show following Y coordinates:
A) Bottom side of Crane collision box
B) Top side of Pushable item collision box
C) Real Y coordinate of the crane
D) Top Y side of floor collision inside the pushable item

The image shows own when the problem occurs: in this moment, the collision box of crane (A) and that of pushable (B) are not yet colliding but the (C) real Y coordinate of the crane is already inside Floor collision (D) of the pushable.
In this situation the code stops the crane because it is touching the floor and there will be no chance to grab the item and neither to discover that crane is going to interact with a pushable item.
This fact to have no collision between two items it's the main problem, because in this way we cann't apply the StartMoveItem() function to remove the floor collision from that item, since we have not yet detected any item.



How to fix the bug about grabbing of the pushable item

To fix this problem we have to use own the bottom side of crane collision box (the B red point in previous picture) instead by using its Y coordinate, to detect the collision with floor.
This operation is easy: just compute the Y bottom side of collision box of the crane, and then using this value to compare with Y floor coordinate.
We have to change the code of Crane_MoveCraneVertically() function.
We define a local variable to host the value of bottom side of crane collision box:

          int YVerticalLimit;
          int BoxBottomY;

Then we add our change only when crane is moving down and the jaws are empty (and open), since it's only in this situation we are managing for grabbing:

                    }else {
                              // no grabbed item: check collision between crane and other objects:
                              // compute the Y coordinate of crane collision box
                              Get(enumGET.ITEM_COLL_BOX, ItemIndex,0);
                              BoxBottomY = NewY + GET.pCollItem->MaxY;

We have computed the value of bottom side of collision box.
The we use this value to compare with Y floor:

                              // check if crane reached the floor
                              // we use the bottom of crane collision box to detect collision with floor:
                              if (BoxBottomY >= FloorY) {
                                        // yes: reached the floor: stop the movement
                                        VSpeed=0;
                                        NewY = FloorY;

                                        // effects to have touched the floor

After these changes the crane should collides with pushable item just a moment before touching the special floor collision inside pushable.
So when we detect a grabbale item, we have immediately to call StartMoveItem() function to remove special collisions.
We'll place this call in same positions where we used the SingalMovedItem() since it used own in the moment when we are to move the grabbed item.

                              case AFO_GRAB_LARGE:
                              case AFO_GRAB_SLIM:
                                        // if there is hard grabbing, verify if there is right animation in progress
                                        if (GET.pItem->OcbCode & CRANE_OCB_HARD_GRABBING) {
                                                  if (GetRelativeAnimation(GET.pItem) == 3) {
                                                            // animation is right
                                                            GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                            IndexGrabbed= COLLIDE.ItemIndex;
                                                            SignalMovedItem(IndexGrabbed);
                                                            StartMoveItem(IndexGrabbed);

Also in automatic grabbing we have to do same call:

                                        // automatic grabbing mode (no CRANE_OCB_HARD_GRABBING flag)...
                                        // to accept the grabbing, it's necessary that crane had open jaws animation:
                                        if (GetRelativeAnimation(GET.pItem) == 5) {

                                                  // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                                  GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                  IndexGrabbed= COLLIDE.ItemIndex;
                                                  SignalMovedItem(IndexGrabbed);
                                                  StartMoveItem(IndexGrabbed);


Changes in CreateFallingDownForItem() function

Also when we start the progressive action to move some item it's better calling the StartMoveItem() because it could be used to move also pushable items:

          GET.pAction->VetArg[4] = IndexItemToIgnore; // index of item to ignore in colliding analys
          GET.pAction->Arg2 = Flags; // Flags FDI

          SignalMovedItem(ItemIndex);
          StartMoveItem(ItemIndex);
}


Remember to call also the EndMoveItem() function at end

The StartMoveItem() removed the special collisions, but we should remember also to restore them, using the EnMoveItem() function.
We should call it everytime we completed the moving of the item.

We release the grabbed item when there is the open jaws command, so we'll change the code also in Crane_InputOthers() function:

                    // if crane was grabbing an item: manage the falling down of the grabbed item
                    if (IndexGrabbed != -1) {
                              if (GET.pItem->OcbCode & CRANE_OCB_FALLING_DOWN_ITEM) {
                                        // to do falling down the item
                                        CreateFallingDownForItem(IndexGrabbed, ItemIndex, 32, 16, 200, -1,
                                                                                          FDI_EARTH_QUAKE | FDI_ENGAGE_ROLLINGBALL | FDI_KILL_ENEMY);
                                        // stop vertical movement of the crane
                                        GET.pItem->Reserved_36 = 0;
                              }else {
                                        // if there is no progressive action the grabbed item is now in final position
                                        // so we call now the EndMoveItem() function
                                        EndMoveItem(IndexGrabbed);
                              }


Also at end of progressive action we should call EndMoveItem:

                              // engage the item if it was a rollingball
                              if (GET.pItem->SlotID == 130) {
                                        if (pAction->Arg2 & FDI_ENGAGE_ROLLINGBALL) {
                                                  // trigger the rollingball
                                                  PerformActionTrigger(NULL, 43, ItemIndex, 0);
                                        }
                              }
                              // disable this progressive action
                              pAction->ActionType = AXN_FREE;
                              // set the end of moving item
                              EndMoveItem(ItemIndex);

                    }


Testing the fixing for pushable item



Ok, now it works, or at least, it seems working fine but since the pushable has special collisions, to be sure that it worked, we need to left the pushable on the ground and then verify if all works fine when lara interacts with it.
So we place the pushable in main room, quit the driving, and go to check how it works.




Very well, Lara is able to walk over the pushable.
But when lara tries to pull it there is a problem:


Lara cann't align correctly, the alignment is endless, and at end it gives up.
When there is this problem it's because the collision box of pushable keep far lara.
But we are not changed the size of collision box...

That weird collision box of pushable items

Investigating about this bug, I discovered a weird feature of collision box of all pushable items.


It is a very little detail...
The collision box is suspended a bit over the floor: in above picture you can see this little space
Really I don't understand the reason but I suppose it was necessary to allow to lara to get very closed to pushable and then pushing/pulling it.

If it was true, perhaps the bug it's own in our code.
When we let falling down the pushable, the code of AXN_FALLING_DOWN_ITEM progressive action, try to place the item on the floor, keeping the bottom side of collision box on the floor.
It has been thought for items like the rollingball, anyway with the pushable item this code will remove that little space between the item and the floor.
We can verify if this theory is right.
In the code of AXN_FALLING_DOWN_ITEM progressive action, (in the PerformMyProgrAction() function), when the item reached the floor, we'll change the Y coordinate in different way when the item is a pushable item:

                    if (BaseY >= FloorY) {
                              // reached the floor
                              // force Y coordinate to be aligned with floor
                              // but if it is a pushable item, force its position = Y floor
                              Slot=GET.pItem->SlotID;
                              if (Slot >= 156 && Slot <= 160) {
                                        // it is a pushable item: force floor coordinate
                                        NewY = FloorY;
                              }else {
                                        // for other item: set Y coordinate to get the bottom of collision box standing on the floor
                                        NewY = FloorY - GET.pCollItem->MaxY;
                              }
                              // perform sound effect?

                              if (pAction->VetArg[3] != -1) {
                                        SoundEffect(pAction->VetArg[3], &GET.pItem->CordX, 0);
                              }

For this new code we have also added new local variable:

          WORD Slot;

We let old method for rollingball and some other moveables, but when the item is a pushable item, we'll set simply its Y coordinate with the same of floor:

                              Slot=GET.pItem->SlotID;
                              if (Slot >= 156 && Slot <= 160) {
                                        // it is a pushable item: force floor coordinate
                                        NewY = FloorY;

Now we retry newly the experiment:
- Grab the pushable
- Move it over the floor of main room
- And let falling down
- Then we go to verify if Lara now is able to push/pull it


Solved another bug about pushable items



Ok, now lara is able to push/pull the item.
So it was really that little space the problem.
The pushable items are very problematic moveables, and those with special floor collisions are yet more problematic.

Saving the game in the middle of pushable movements

Everytime we create a new skill we should always verifying what happens when we save the game in the middle of this action and then reload it.
If we perform this test while the pushable item is grabbed by the crane and then we reload the savegame we can see what happens...

We saved the game while the crane was moving from right to left, grabbing the pushable:



Then we reload that savegame, let the crane moved yet one sector at left, and then we move down the crane.
When it is at half height we open the jaws (SPACE) and we let falling down the pushable:




First bug: the pushable doesn't stop on the floor but it disappears, sinking in the floor.

Now we quit the driving and go to see what happened...



We find other problems: there is an invisible wall where lara bumps (left picture) and then we have the confirm that it is a floor collision, where the mummy moves over (right picture).

This chaos it happened for this reason:
  1. When the crane grabbed the item, we removed floor collision with StartMoveItem()
  2. Then we saved the game, while the crane was grabbing the pushable item
  3. When we reloaded the savegame, the trng engine enabled special floor collisions for all pushable items of the level, included our pushable item. But it is suspended in the empty and the egine will add a floating floor collision below where it is the pushable when the savegame it has been just loaded.
  4. When we let falling down the pushable on another sector, the collision of the whole room are so messed that the item falls in an invisible hole.

The problem in above sequence is when the game will be reloaed and the pushable item is not on the floor, in right position, but floating in the empty.
I had already had to fix this problem when with some trng action trigger I moved pushable item.
The trick is to change the ocb code of this pushable item at begin of movement.
In this way, also if the game will be saved and then reloaded, when the engine reload the pushable item, will not find the ocb required to create a floor collision and it will ignore that pushable item.
Then, when the movement it has been completed, the code will restore the original value of pushable ocb code.
Now we have to do same work: one for when pushable item is grabbed by the crane, and another when the pushable is handled by progressive action to do fall down it.


The changes for crane management of pushable item

How explained, this code should save the flag (for pushable items) that enables special ng features , the OPU_ENABLE_NG flag (value 0x0040), and then remove it.
This operation should be done immediately after having called StartMoveItem().
Then, when crane will leave the item, we should do the opposite: check if previous saved flag was present, and in that case, add it newly to ocb field of pushable item.
We can create a little function to reach this target, the same for start moving and end moving.
We'll use an input argument to signal if we are using this function at start (TestStart == true) or at end (TestStart==false) of the moving.

void Crane_ManagePushableItem(StrItemTr4 *pCrane, int GrabbedItemIndex, bool TestStart)
{
          StrItemTr4 *pSalva;

          // save previous value of GET.pItem
          pSalva = GET.pItem;

          Get(enumGET.ITEM, GrabbedItemIndex, 0);

          if (GET.pItem->SlotID < 156 || GET.pItem->SlotID > 160) {
                    // the item is NOT a pushable: quit the code
                    // restore the GET.pItem
                    GET.pItem = pSalva;
                    return;
          }
          // the grabbed item is a pushable
          if (TestStart == true) {
                    // we started now to move the item
                    if (GET.pItem->OcbCode & OPU_ENABLE_NG) {
                              // pushable has ng feature:
                              // rememebr in crane structure with CRANE_REMOVED_NG_FROM_PUSH flag, that we had
                              // removed ng features from pushable:
                              pCrane->Reserved_3A |= CRANE_REMOVED_NG_FROM_PUSH;
                              // and now remove the OPU_ENABLE_NG flag from ocb of pushable item:
                              GET.pItem->OcbCode &= ~OPU_ENABLE_NG;
                    }
          }else {
                    // we completed now the moving of the item
                    // if we had removed the OPU_ENABLE_NG flag, now we set it newly
                    if (pCrane->Reserved_3A & CRANE_REMOVED_NG_FROM_PUSH) {
                              // enable newly in pushable item the OPU_ENABLE_NG flag
                              GET.pItem->OcbCode |= OPU_ENABLE_NG;

                              // now remove also the CRANE_REMOVED_NG_FROM_PUSH flag to avoid that it messed something in future grabbings
                              pCrane->Reserved_3A &= ~CRANE_REMOVED_NG_FROM_PUSH;
                    }
          }
          // restore the GET.pItem
          GET.pItem = pSalva;
}

We have created a new crane flag to remember when the OPU_ENABLE_NG ocb flag was present:

#define CRANE_REMOVED_NG_FROM_PUSH 0x0040

Note: it's only a coincidence that this new flag had same value of OPU_ENABLE_NG ocb flag.

Now we'll call above function after the StartMoveItem() function called from crane code.
Two adding it will be done in Crane_MoveCraneVertically() function:

                              case AFO_GRAB_LARGE:
                              case AFO_GRAB_SLIM:
                                        // if there is hard grabbing, verify if there is right animation in progress
                                        if (GET.pItem->OcbCode & CRANE_OCB_HARD_GRABBING) {
                                                  if (GetRelativeAnimation(GET.pItem) == 3) {
                                                            // animation is right
                                                            GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                            IndexGrabbed= COLLIDE.ItemIndex;
                                                            SignalMovedItem(IndexGrabbed);
                                                            StartMoveItem(IndexGrabbed);
                                                            Crane_ManagePushableItem(GET.pItem, IndexGrabbed, true);

Above was for hard grabbing, now in same Crane_MoveCraneVertically() function, we did the same for automatic grabbing:

                                        // automatic grabbing mode (no CRANE_OCB_HARD_GRABBING flag)...
                                        // to accept the grabbing, it's necessary that crane had open jaws animation:
                                        if (GetRelativeAnimation(GET.pItem) == 5) {

                                                  // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                                  GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                  IndexGrabbed= COLLIDE.ItemIndex;
                                                  SignalMovedItem(IndexGrabbed);
                                                  StartMoveItem(IndexGrabbed);
                                                  Crane_ManagePushableItem(GET.pItem, IndexGrabbed,true);
                                                  // set animation of grabbing


Above calls were all with TestStart=true, since there was the grabbing of the item, the start of moving.
Then we have to change also the code when the crane leaves the grabbed item, and it happens in Crane_InputOthers() function, when the open jaws command has been used.

                    // if crane was grabbing an item: manage the falling down of the grabbed item
                    if (IndexGrabbed != -1) {
                              // the management of grabbed item from crane quits here
                              Crane_ManagePushableItem(GET.pItem, IndexGrabbed,false);

                              if (GET.pItem->OcbCode & CRANE_OCB_FALLING_DOWN_ITEM) {
                                        // to do falling down the item
                                        CreateFallingDownForItem(IndexGrabbed, ItemIndex, 32, 16, 200, -1,
                                                                                          FDI_EARTH_QUAKE | FDI_ENGAGE_ROLLINGBALL | FDI_KILL_ENEMY);
                                        // stop vertical movement of the crane
                                        GET.pItem->Reserved_36 = 0;
                              }else {
                                        // if there is no progressive action the grabbed item is now in final position

                                        // so we call now the EndMoveItem() function
                                        EndMoveItem(IndexGrabbed);
                              }

Please note that we called the Crane_ManagePushableItem() function, before calling EndMoveItem() function, because when we call StartMoveItem() or EndMoveItem() it's necessary that ocb code of pushable item had correct values.
Other interesting fact, it's that we call the Crane_ManagePushableItem() function, also before passing the item to the progressive action to do fall it.
It's necessary because the management of Crane_ManagePushableItem() is only for crane, while for the progressive action we should use another method, because this action should be independent since in future it could be called by other code, different than crane management.


The changes for pushable item management from progressive action

Since in the structure of progressive action we have more space for global variables, in this case we could use an easier method.
While with crane we used a single flag to remember the value of other ocb flag and then restore it, now we can simply copy the whole ocb field of item, then clear the ocb field, and at end of moving, restoring the original ocb value we had saved.
So in the creation of progressive action we save the original ocb value and then clear the ocb field of item, no matter if it is a pushable item or less.

          GET.pAction->VetArg[4] = IndexItemToIgnore; // index of item to ignore in colliding analys
          GET.pAction->Arg2 = Flags; // Flags FDI

          SignalMovedItem(ItemIndex);
          StartMoveItem(ItemIndex);
          // save pointer of GET.pItem
          pSave=GET.pItem;

          // save ocb value of item in VetArgShort[10]
          Get(enumGET.ITEM, ItemIndex,0);

          GET.pAction->VetArgShort[10] = GET.pItem->OcbCode;
          // and now clear the ocb code, in this way if it was a pushable object with special feature we disabled them
          GET.pItem->OcbCode = 0;

          // restore GET.pItem
          GET.pItem = pSave;
}

Since we used Get() for item, we saved temporarily also the previous value of GET.pItem and for this target we had defined another local variable:

          StrItemTr4 *pSave;


Then we add the code to restore original ocb value when the item touched the ground.
This happen in case AXN_FALLING_DOWN_ITEM section of PerformMyProgrAction() function, of course:

                              // engage the item if it was a rollingball
                              if (GET.pItem->SlotID == enumSLOT.ROLLINGBALL) {
                                        if (pAction->Arg2 & FDI_ENGAGE_ROLLINGBALL) {
                                                  // trigger the rollingball
                                                  PerformActionTrigger(NULL, 43, ItemIndex, 0);
                                        }
                              }
                              // disable this progressive action
                              pAction->ActionType = AXN_FREE;
                              // restore ocb value of item
                              GET.pItem->OcbCode = pAction->VetArgShort[10];

                              // set the end of moving item
                              EndMoveItem(ItemIndex);

                    }


Note: we used the VetArgShort[10] field of progressive action to save the ocb value, because ocb field is a "short".
We used the index "10" because first 0-9 indices was used by VetArg[0/1/3/4] field, since the memory zone is the same (overlapped) and VetArg[] are "int" type, i.e. 4 bytes and one of them used 4 bytes or 2 short values.

Now we can build the library and test in game, saving in the middle when the crane is grabbing the pushable item, but also (another test) when the crane left the item and it is the progressive action to manage it.


Fixing the landing of the grabbed item

Previous fixing went fine, now also pushable items with special ng features can be moved without collateral effect.
Now we have to fix yet some little details.
When there is crane ocb (CRANE_OCB_FALLING_DOWN_ITEM) to let falling down the item, we have our progressive action, anyway we should improve a bit when that flag is missing.
We should adjust the final Y coordinate of grabbed item in same way we did in progressive action: if the item is a pushable we should force its Y coordiante at same value of the floor, while for other items, we let that bottom collision box was on the floor.
Another change is when there is the CRANE_OCB_FALLING_DOWN_ITEM flag, but however the grabbed item it has been placed on the ground.
In this situation is stupid call the progressive action to do fall down it by 0 units and listen also the roumble and earth-quake in spite it was already on the ground.
So we change the code in Crane_InputOthers() function in this way:

                    // if crane was grabbing an item: manage the falling down of the grabbed item
                    if (IndexGrabbed != -1) {
                              // the management of grabbed item from crane quits here
                              Crane_ManagePushableItem(GET.pItem, IndexGrabbed,false);

                              // discover if it is reasonable to do fall down the grabbed item
                              TestFallDown=false;
                              NumSound=-1;
                              Flags = FDI_ENGAGE_ROLLINGBALL;
                              // save GET.pItem (with crane structure)
                              pSave=GET.pItem;

                              // get structure of grabbed item
                              Get(enumGET.ITEM, IndexGrabbed,0);
                              // only if distance between bottom side from ground is greater than 100 game units
                              // we use the progressive action, otherwise we'll simply adjust its final position
                              Get(enumGET.ITEM_COLL_BOX, IndexGrabbed,0);

                              BottomY = GET.pItem->CordY + GET.pCollItem->MaxY;
                              CheckFloor(pSave->CordX, pSave->CordY, pSave->CordZ, pSave->Room);
                              Distance = FLOOR.FloorHeight - BottomY;

                              if (Distance > 100) {
                                        TestFallDown=true;
                              }

                              if (Distance > 500) {
                                        Flags |= FDI_EARTH_QUAKE | FDI_KILL_ENEMY;
                                        NumSound = 255;
                              }
                              // restore crane structure
                              GET.pItem = pSave;

                              if (TestFallDown==true && (GET.pItem->OcbCode & CRANE_OCB_FALLING_DOWN_ITEM)!=0) {
                                        // to do falling down the item
                                        CreateFallingDownForItem(IndexGrabbed, ItemIndex, 32, 16, 200, NumSound, Flags);
                                        // stop vertical movement of the crane
                                        GET.pItem->Reserved_36 = 0;
                              }else {
                                        // if there is no progressive action the grabbed item is now in final position
                                        // anyway we adjust its position if it is a pushable item:
                                        if (GET.pItem->SlotID >= enumSLOT.PUSHABLE_OBJECT1 && GET.pItem->SlotID <= enumSLOT.PUSHABLE_OBJECT5) {
                                                  // it is a pusable item: force Y coordinate at same value of current floor
                                                  GET.pItem->CordY = FLOOR.FloorHeight;
                                        }

                                        // so we call now the EndMoveItem() function
                                        EndMoveItem(IndexGrabbed);
                              }

                              // remove data for grabbed item
                              IndexGrabbed=-1;
                              GET.pItem->Reserved_38 = -1;
                    }          

For above code we define some new local variables:

          bool TestFallDown;
          WORD Flags;
          int BottomY;
          int Distance;
          StrItemTr4 *pSave;
          int NumSound;

We computed the distance between bottom side of collision box (of grabbed item) and then we used this value to set if using the progressive action or less.
When the distance was greater than 100 game units, we enable the usage of progressive action:

                              if (Distance > 100) {
                                        TestFallDown=true;
                              }

We used the distance also to enable the earth-quake only when distance from ground is greater than 500 game units.
We enable the FDI_KILL_ENEMY also for same distance, since, when the distance is less probably the collision it's only on side and assign a sound effect when there is hearth-quake:

                              if (Distance > 500) {
                                        Flags |= FDI_EARTH_QUAKE | FDI_KILL_ENEMY;
                                        NumSound = 255;
                              }


Then we enabled the progressive action only when there are in same moment two true conditons: the distance is good (we knew that reading TestFallDown variable) and the falling down it has been enabled in crane ocb field:

                              if (TestFallDown==true && (GET.pItem->OcbCode & CRANE_OCB_FALLING_DOWN_ITEM)!=0) {
                                        // to do falling down the item
                                        CreateFallingDownForItem(IndexGrabbed, ItemIndex, 32, 16, 200, NumSound, Flags);


While, when we do not use the falling down, we'll adjust the Y coordiante of item if it is a pushable item:

                                        // if there is no progressive action the grabbed item is now in final position
                                        // anyway we adjust its position if it is a pushable item:
                                        if (GET.pItem->SlotID >= enumSLOT.PUSHABLE_OBJECT1 && GET.pItem->SlotID <= enumSLOT.PUSHABLE_OBJECT5) {
                                                  // it is a pusable item: force Y coordinate at same value of current floor
                                                  GET.pItem->CordY = FLOOR.FloorHeight;
                                        }


Now we can test the code and verify what happens when the crane move slowly the grabbed item until the ground and we open the jaws in that moment.


Changing the collision of crane when it is drivable

Now we try this little experiment:
- Drive the crane and move down it touches the floor
- Now quit the driving and move lara closed to the crane on the floor


That happens it's not normal. Lara pass across the crane like it was incorporeal and she dies immediately.
This behaviour was good for the automatic crane that had as only one target to fall down to kill lara, but the drivable crane it's different. When lara is able to touch it, the crane is surely still and there is no reason that a metal stick had above effects on Lara.
So we should change the CollisionCrane() function to manage in different way the collision if the crane is working in manual (drivable) way. In this case it will not injury lara it keeps simply her away from its collision box.

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

          // if crane is working in manual way (drivable crane) do not injury lara but only push away her:
          if (GET.pItem->OcbCode & CRANE_OCB_MANUAL) {
                    // manual crane: push away lara
                    ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);                    
          }else {
                    // crane is not manual: injury lara

                    // lara and crane are in collision
                    // reduce health of lara (at least if she was not already dead)
                    if (pLara->Health > 0) {
                              // toggle HP value
                              pLara->Health -= 100;
                              // sound for LARA_INJURY
                              SoundEffect(31, &GET.pLara->CordX, 0);
                              // show HP bar on the screen
                              pLara->FlagsMain |= 0x10;
                    }
          }


Testing new crane collisions



Ok, now crane doesn't injury lara and lara is not able to pass across it.
We can add new sound effects for some operation of the crane:


151 CHAINS_LIBRARY (while crane is moving up/down, grabbing an item)
283 OBJ_BOX_HIT (When crane, moving down, touch the floor with the grabbed item)


We place all sound effect in void Crane_MoveCraneVertically() function:

                              // restore all data
                              GET.pSlot->pProcCollision = pProcColl;
                              GET.pItem = pCrane;
                              
                              // if there is collision: stop here
                              if (TestCollision) {
                                        // restore old Y coordinate of crane
                                        NewY= GET.pItem->CordY;
                                        // and clear vertical speed
                                        VSpeed=0;
                                        // if only in this moment the grabbed item touched the floor: play sound effect 283 OBJ_BOX_HIT
                                        if ((GET.pItem->Reserved_3A & CRANE_ITEM_ON_FLOOR) ==0) {
                                                  SoundEffect(283, &GET.pItem->CordX, 0);
                                        }
                                        // inform that item reached the floor
                                        GET.pItem->Reserved_3A |= CRANE_ITEM_ON_FLOOR;
                              }
                              // no collision with floor and items, but check if crane reached max vertical limit

Above code is when crane is moving down with a grabbed item and the item touches the floor.

Then we add the sound effect when the crane is moving up or down but not when it is falling down:

          // update values:                    
          GET.pItem->Reserved_36 = VSpeed;

          // add chain sound if crane is moving slowly up/down while is grabbing and item
          if (VSpeed != 0 &&
                    (GET.pItem->Reserved_3A & CRANE_FALLING_DOWN)==0 &&
                    GET.pItem->Reserved_38 != -1) {
                    // crane is moving slowly and is grabbing an item

                    SoundEffect(151, &GET.pItem->CordX, 0);
                    
          }

          // compute the differnce in Y coordinate of crane and apply same change for grabbed item


Problems with rollingball item



We had already tried to grab a rollingbal and let falling down on a slope. We used the FDI_ENGAGE_ROLLINGBALL flag for progressive action, to trigger the rollingball and having it rolling really after the falling down.
It worked fne, but if we try to grab newly the rolling ball we have a bad surprise...


We grab newly the rolling ball (A picture), but when we pull up the crane the rolling ball remains on the ground. When we move horizontally the crane the rollingball follows its moving but always standing on the ground (B and C pictures).
It happens because now the rollingball it's already enabled and its control procedure works to change its Y coordiante to simulate gravity, letting always on the ground the item, in spite the crane tried to pull up.
To solve this problem we need untrigger the rollingball before grabbing it, to exclude its control procedure while the crane is moving it.
Then we'll trigger newly it, like it already foreseen if there is the FDI_ENGAGE_ROLLINGBALL flag, by the way.

We can create a little function to manage this situation, since we need of this code twice: one for hard grabbing and another for automatic grabbing.

// called when it has been just grabbed an item
// check if the item is a rollingball, and if it has been already triggered: untrigger it
void Crane_CheckRollingball(int ItemIndex)
{
          StrItemTr4 *pSave;

          // save current GET.pItem
          pSave = GET.pItem;

          Get(enumGET.ITEM, ItemIndex,0);

          if (GET.pItem->SlotID == enumSLOT.ROLLINGBALL) {
                    // verify if it is triggered
                    if (TriggerActive(GET.pItem)==true) {
                              // it is a rolling ball and it has been triggered: now untrigger it
                              // we use action trigger 44: "Trigger. (Moveable) Untrigger <#>Object with (E)Timer value"
                              PerformActionTrigger(NULL, 44, ItemIndex, 0);
                    }
          }
          GET.pItem = pSave;
}

Then we call this function passing the index of grabbed item, when the crane is going to grab it.
So we add these calls in the code of Crane_MoveCraneVertically() function.
Once for hard grabbing:

                              case AFO_GRAB_LARGE:
                              case AFO_GRAB_SLIM:
                                        // if there is hard grabbing, verify if there is right animation in progress
                                        if (GET.pItem->OcbCode & CRANE_OCB_HARD_GRABBING) {
                                                  if (GetRelativeAnimation(GET.pItem) == 3) {
                                                            // animation is right
                                                            GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                            IndexGrabbed= COLLIDE.ItemIndex;
                                                            SignalMovedItem(IndexGrabbed);
                                                            StartMoveItem(IndexGrabbed);
                                                            Crane_ManagePushableItem(GET.pItem, IndexGrabbed, true);
                                                            // now we have to rememebr that the grabbing phase begun:
                                                            GET.pItem->Reserved_3A |= CRANE_GRABBING_IN_PROGRESS;
                                                            // verify if it requires slim grabbing
                                                            if (Flags == AFO_GRAB_SLIM) {
                                                                      // yes, set flag to remember that requires slim grabbing
                                                                      GET.pItem->Reserved_3A |= CRANE_REQUIRES_SLIM_GRAB;          
                                                            }
                                                            // set that currently the grabbed item is yet on the floor
                                                            GET.pItem->Reserved_3A |= CRANE_ITEM_ON_FLOOR;
                                                            // check if it's necessary untrigger rollingball
                                                            Crane_CheckRollingball(IndexGrabbed);

                                                  }else {
                                                            // animation is wrong: to do fail the grabbing
                                                            // consider item only as floor collision
                                                            FloorY=ObjectTopY;
                                                  }
                                                  break;


And another for automatic grabbing:

                                        // automatic grabbing mode (no CRANE_OCB_HARD_GRABBING flag)...
                                        // to accept the grabbing, it's necessary that crane had open jaws animation:
                                        if (GetRelativeAnimation(GET.pItem) == 5) {

                                                  // set to have grabbed the item (surely a moveable) with index in COLLIDE.ItemIndex
                                                  GET.pItem->Reserved_38 = COLLIDE.ItemIndex;
                                                  IndexGrabbed= COLLIDE.ItemIndex;
                                                  Crane_CheckRollingball(IndexGrabbed);
                                                  SignalMovedItem(IndexGrabbed);
                                                  StartMoveItem(IndexGrabbed);
                                                  Crane_ManagePushableItem(GET.pItem, IndexGrabbed,true);
                                                  // set animation of grabbing
                                                  if (Flags == AFO_GRAB_LARGE) {
                                                            // from open to large grabbing
                                                            ForceAnimationForItem(GET.pItem, 3, 2);
                                                  }

Now we build the library and verify what happens moving for second time the already triggered rolling ball...

Testing multi grabbing for rolling ball



This time it works...
The crane is able to have up the rolling ball and then we can get falling down it newly and again it will be triggered and it will roll for the slope.

Alignmment of position of the rolling ball

Looking above image you see that the rollingball while it grabbed by crane, it's well aligned but a bit moved al left. It happens because also its original position, in grabbing phase, it was not at center of sector. With rollingball may happen because it rolls and stops in any position, not only at center of sector.
For this reason, the crane sometimes will be not able to grab it, when it is too moved on side, but now we could fix at least another problem: when the crane is grabbing the rollingball, we could change its X and Z coordinate to aling it with center of current sector.
In game it will seem reasoable that the jaws, closing it, move a bit the rollingball, and then we'll have it well aligned at center of jaws.
We can place this code in our function to manage rollingball grabbing:

void Crane_CheckRollingball(int ItemIndex)
{
          StrItemTr4 *pSave;

          // save current GET.pItem
          pSave = GET.pItem;

          Get(enumGET.ITEM, ItemIndex,0);

          if (GET.pItem->SlotID == enumSLOT.ROLLINGBALL) {
                    // we align its horizontal position moving it at center of current sector.
                    // since the crane is surely at center of sector, we can use its X and Z coordinates
                    // to align also the rollingball (pSave hosts now the crane structure)
                    GET.pItem->CordX = pSave->CordX;
                    GET.pItem->CordZ = pSave->CordZ;

                    // verify if it is triggered
                    if (TriggerActive(GET.pItem)==true) {
                              // it is a rolling ball and it has been triggered: now untrigger it
                              // we use action trigger 44: "Trigger. (Moveable) Untrigger <#>Object with (E)Timer value"
                              PerformActionTrigger(NULL, 44, ItemIndex, 0);
                    }
          }
          GET.pItem = pSave;
}

We performed the alingment, indifferently if the rollingball is engaged or less.

Now we can verify in game if it works...



If you look carefully, you see that rollingball, before the crane grabbed it (left picture), it's a bit moved at left, while after the grabbing it is own at center of current sector (right picture).
The alingment works...


Type the description of OCB codes in our .ocb file

As promised, now we completed the code of drivable crane, we should add a full description for all ocb codes.


The .ocb file of our plugin
Since we are talking about ocb codes, we should type the description in our .ocb file, i.e. a text file with name of our plugin but with extension ".ocb".
We'll place this file in the folder of NG_Center program and the program it will load it (only at first launch of the program) and it will insert the description of this .ocb file, in the "OCB List" of reference panel, with all others.
The internal syntax of .ocb file it's easy:
Just type the special tag "#START_TAG#=" followed by the name of object whose you are describing the ocb values, and then, in following rows, you type all text you wish with any syntax to describe this object and its ocb codes.
Note: differently by mnemonic constant of ".script" file, in this case you no need to compact the rows, removing newline character with ">" characters.
In the ".ocb" file the test is plane, with no special conversion.
This is the text we'll add to our "plugin_....ocb" file:

#START_TAG#=_NEW Swinging Crane
Before describing the OCB codes of this new object, it's important remember that it's necessary using some AssignSlot= scritp command to do work it.
If you means using only the "automatic" crane, it's enough only one AssignSlot script command:

AssignSlot= UsedSlot, OBJ_SWINGING_CRANE

Where you replace "UsedSlot" with number (or name) of slot where this object has been stored in your wad file.
For instance if the Swinging Crane is in ANIMATING2 slot, you'll type in right [level] section the command:

AssignSlot=ANIMATING2, OBJ_SWINGING_CRANE

In the case you mean using the manual (drivable) crane, you need to type two AssignSlot= command: One it has same meaning of above, while the other will be to inform the plugin where you stored the Control Panel used to engae your drivable crane.
To signal the control panel of the crane you use the OBJ_CONTROL_PANEL_CRANE constant.
For instance:

AssignSlot=          ANIMATING2, OBJ_SWINGING_CRANE
AssignSlot=          ANIMATING3, OBJ_CONTROL_PANEL_CRANE

OCB CODES FOR SWINGING CRANE
----------------------------
OCB:1 Automatic crane. With this ocb the crane will work like in tomb raider 3, it will follow lara from the ceiling and when it is over lara sector will fall down trying to injury or kill her.
In automatic mode it's not used a control panel object.

OCB:2 Manual Crane. When you set 2 (and you omit the ocb 1) the crane will be droven by Lara.
In this working mode, you have to place in the level also the Control Panel item.
It can be any item, theoratically, since it is not required that it had special animations or features, anyway lara will use this item to engage the crane, like it was a switch object.

There are three animations used to simulate the activation of the crane by Lara, while she is in front of control panel:

Animation 445: Lara hits some keys on keyboard and then grabs the joystick to control the crane. This is the "get in" driving crane.
Animation 446: Lara is almost still, only very little movement. This animation will be performed continuosly until we are in "driving crane" mode.
Animation 447: Lara move from above 446 to standard stand-up position. This is the "get off" driving crane animation.

Notes:

- Remember to type in Object Code value of Control Panel item, the index of Crane item that the control panel should engage.

- Remember to add to current [level] section an AssignSlot command with OBJ_CONTROL_PANEL_CRANE constant to singal the slot where you put the control panel item.


You can change these animations in according with type of control panel you mean using, of course.

See at bottom of this text the description of input game commands used to drive the crane.

OCB:4 Shadow. This ocb will draw a shadow on the ground below the crane.

OCB:8 Proportional Shadow. This ocb works only togheter with OCB:4. When you enabled a shadow wiht OBC:4, then you can get it proportional adding also OCB:8.
A proportional shadow is that shadow that changes its size in according with the distance of its item. This means that the shadow will be littler, when the crane is hanged at the ceiling, while it will become larger when the crane is moving down, getting closer to the floor.

OCB:16 Hard Grabbing. The drivable (manual) crane is able to grab items and move them in other sides. When you omit this OCB:16 the grabbing is automatic: everytime the crane move down over a gabbable item, the crane will close its jaws and will grab that item. Anyway if you wish that this grabbing required some game skills, you can add the OCB:16 and in this case the player should send "close jaws" command in right moment, otherwise the grabbing will fail.

OCB:32 Enable falling down of grabbed item. If you omit this OCB:32 code, once the crane grabbed an item, the player should move the item until to reach the floor to be able to leave it in other position. Differently, if you use the OCB:32 code, the player will be able to do falling down the item also when the crane is floating in the empty, very far by ground.

OCB:Value*256 Max Vertical Distance. You can set the max distance that the crane will be able to reach from ceiling. This is a value in sectors and you type it with following formula:

MaxVerticalDistance * 256 + OCB flags

Note: if you don't type any max vertical distance, it will be used 5 sectors as default value.

GAME COMMANDS TO DRIVE THE CRANE
--------------------------------
ACTION(ctrl) : Keeping down the action key, the FORWARD(UP)/ BACKWARD(DOWN) commands will be used to move up/down the crane slowly

DRAW WEAPON (space): Open the jaws and left falling down the grabbed item. If currently there is no grabbed item, nothing it will be performed

JUMP(alt): Left falling down the crane but only when there is not grabbed item

WALK(shift): Try to close the jaws to grab some item.

ROLL(end): Quit the driving crane mode, and returns to common game playing

Camera Mode
-----------
Since that when player drives the crane, it's not lara the leading actor, you should change the camera mode to look only the crane, rather lara.
You can using a fixed camera or some cutscene cameras, anyway to know the moment to perform the triggergroup to engage the camera mode you should use a GlobalTrigger= command of GT_DRIVING_CRANE_START type, while when the crane camera should be removed you can use a GT_DRIVING_CRANE_QUIT global trigger to restore common lara's camera mode.

Ok, it's very long text but it's necessary to explain all matters about this new object.
Note: I added "_NEW" prefix before typing the name:

#START_TAG#=_NEW Swinging Crane

This prefix is not necessary and neither it has some meaning about syntax. It's only because in "OCB List" I had the habit to put the "_NEW" prefix in front of codes for new objects, so, since the swinging crane is a new object, I used again this rule.


Description for Control Panel OCB codes

In spite the Control Panel for crane, it's an easier object, we should remember to type a section in our .ocb file, also for it, since there is an important value to type in Object code of this item.
So we add to our .ocb file also this text:

#START_TAG#=_NEW Control Panel for Crane
In the object you use as Control Panel for crane, you have to type as OCB code the index of crane item that this control panel will engage.


Now we save our .ocb file in NG_Center folder: quit NG_Center program and the launch it newly to load our plugin files.
Then we can verify if in "OCB List" of Reference panel there are these new ocb settings.





Description of our new object is present.
You can see that NG_Center added to the description a little info to remember that this ocb it has been created by our plugin:

FROM: Plugin_trng


Homeworks for drivable Crane


Set new ways to interact with other special moveables
We have already handled collisions with creatures, shatter statics, pushable items and rollingballs but you could add code to handle the collision with other special items.
For instance:
  1. If the item is a trapdoor, what should it happens?
    If the door is close, and crane is moving down slowly, it should be stopped from that closed door. But when the door is open, the crane should be able to ignore it and continue to move down.
    By other hand, when the crane is falling down, or its grabbed item, and it bumps against the door and it was close, it should be open for this impact. So we should trigger it to open.

  2. If the item is SMASHABLE_BIKE_FLOOR item, when the crane is falling down (or its grabbed item) it should crash the floor and then continue its falling.

  3. If the item is a TEETH_SPIKES item, the crane could trigger it touching the floor. So we could enable heavy triggers in that sector when the crane is touching the floor.
    You can use the tomb raider function: TestTriggers() to enable trigger on a given sector. If you mean enable only heavy trigger you have to set one of its parameter as "1":

    TestTriggers(void *pFloorDataNow, int TestHeavy, int TestScale);

    Example to trigger heavy triggers in sector where is the crane, supposing that crane structure is in GET.pItem:

              CheckFloor(GET.pItem->CordX, GET.pItem->CordY, GET.pItem->CordZ, GET.pItem->Room);
              TestTriggers(FLOOR.pFloor, 1, 0);

    In this speech, it's interesting the idea to add a new ocb for crane that it will enable heavy triggers everytime it touches the floor.

  4. If the item is a flame emitter, already triggered, and crane is let falling down its grabbed item, perhaps the item could be able to put out the fire.
    Note: in this case you'll have the problem that there is no collision with flame emitters, so you cann't find that item with IsCollidingWithSomeItem() function, but you should use the Find() function to locate all moveable items when the crane is touching the floor. If one of this item is a flame emitter, you should test if it has been triggered, and in that case you can untrigger it to put out the fire.



To improve the sound effects
You can add other sound effect for some dynamic changes:
- When the crane reaches the ceiling and stop its movement, we can suppose a "clang" sound
- Another sound (or the same) when the crane is moving horizontally and it bumps on some border of ceiling stopping its movement
- A sound, very slow, when there is grabbing animation

Other improvement about sounds, could be that to do work fine some sounds, like the 151 CHAINS_LIBRARY, that doesn't work fine.
It is a problem that happens very often when a sound it will be continuosly played. The sound will restart always from begin, getting a bad effect.
To solve this problem it should be necessary: or to discover a flag for SoundEffect() function (third parameter) that to do work looped sounds.
Or try to call the SoundEffect() function only every some interval, to let the time to previous playing to be completed.
To perform a function (or any other code) only every tot frames, you can use a code like this:

          Get(enumGET.GAME_INFO,0,0);

          if ((GET.GameInfo.FrameCounter & 0x2f) == 0x2f) {
                    // perform this code only once every 47 frame (0x2f = 47)
                    SoundEffect(151, &GET.pItem->CordX, 0);
          }

Above code read the main frame counter, that it will be increased by 1 every new played frame, and get only lower bit with 0x2f mask. Then the condition it will be true only when the value is the same of the mask. It will happen once for each Mask frames.
With above code the soundeffect it will be called about once every 1.5 seconds.




Exercise 7: The Mechwarrior

This exercise begins in room 30 of plugins project


Another robot? No, the Mechwarrior is a vehicle! Lara will be able to get in, and drive it. This object it has been made by Psiko: meshes and animations. We'll try to add a code to do move it in the game, interacting with collisions and other items.
It is an interesting exercise because it could be common that, in the creation of some new object, we begin creating its meshes and animations and then we'll have to supply the code to animate it.
In the logic of growing difficulty level, this exercise is surely the most complicated.
This if first time we build a real vehicle, since the drivable crane was only a pseudo-vechicle, with some features of vehicles but with the fact it was anyway a single object, while with real vehicles we'll have to move Lara and the vechicle in same moment, synchronizing the two objects.
In particular way the first get-in animation it will require some attempt to have better coordinating in first fusion of the two objects.
Other complication is that for first time we'll have to compute the collision not only for a single sector but in more generic way, since this vehcile will be able to steer everywhere and not only standing at center of current sector.
More, this is very big object and for this reason the check for collision on a single point it is not enough: we'll have to check different points of its borders to avoid that one of them collided with walls or other items.



The get-in animation

First step for the vehicle code it's always the management of get-in animation.
We call "get-in animation" that animation where lara move on board of the vehicle. This is the moment, when this animation it has been completed, when the driving mode begins. First there are two distinct objects: Lara and the vehicle, while after this animation we'll have a single object, in spite it will be yet arranged by these two objects, but they will have always same position and animation numbers.
This is a delicate phase, and we'll have to do some attempt to find the ideal position when engage the animation of lara, because a little misplacement could be very boring and visible.

In mechwarrior the get-in animation has number 0.
We should look this animation in advance, while she has the vehicle in right position, to understand the relative position to assign in our testposition data.
A little trick is to move temporarily the two objects: the vechile and the slot with lara's animations for that vehicle, in another wad, placing the vehicle in MOTORBIKE slot, while the lara's animations in VEHICLE_EXTRA slot. Then we build a tr4 file with that wad, and at end we'll load this tr4 file in TrViewer.
The reason of this procedure is because TrViewer will show the animation of lara and of the vechile with the two objects pasted togheters, like it should happen in game.


Performing above procedure, I got above image (you should stop the automatic animation to have the chance to choose the right starting frame)
Ok, it's not so clear, anyway we can keep a look on this image when we'll try the ideal position data with LogItem script command in game.


Fixing the collision box for get-in animation


If we see the original collision box (at left) of animation 30 (when the mech is waiting that lara gets in) we see, comparing it with previous picture, that the collision box is too big: lara will be not able to get closer to reach ideal position.
It happens very often using the automatic creation of collision box in Animation Editor.
For most animations it's not a problem but for this animation we have to fix the collision box, reducing its size (picture at right), otherwise lara in game it will be kept too far to reach ideal position.

We can use the FexAnim tool by Turbo Pascal to fix a single collision box of some animation.
Once you changed the collision box of animation 30, you save the .tr4 file, then you use wad merger program, to move from this tr4 file to your wad the object, replacing the old.
In this way also in your wad you'll have the object with fixed collision box.
Note: You find already made this work in plugin.wad , anyway I had to explain this procedure because it could be very often necessary when you are creating a new vehicle.



The standard procedures for Mechwarrior

Before typying code to manage the get-in animation, we have to link this object with our code.
We have already seen how to do...
About the mechwarrior the code to initialise its slot is almost the same we did with Crane object.

While for the lara's animation slot it's another matter, because that slot works only as storage for animations but this "object" will have no collision, draw or control procedure.


News about slot assignment and WadMerger

In previous chapter, about Swinging Crane object, I said that the available slots were all used because after slot number 500, wadmerger had a bug that forbids to use other slots.
The news is that I fixed that bug in wadmerger (and many other) creating a new patched release of Wadmerger.
For this reason, now we can use a new fixed and exclusive slot for mech warrior and its lara's animations.
So in this case we'll not use the AssignSlot method.
Anyway I prefer let that method for Swinging Crane, because it's important that you knew how to manage this situation, since it will be surely necessary in the future, since that, neither now, the available slots are unlimited.
Therefor, when you'll need to create a new object and you cann't (or you don't wish) use an old slot, you'll have to use the AssignSlot method to manage this new object in variable way, where the level builder will choose to place it.
Above speech is for future new objects when there will be no more free slots to host them.
While now, for mechwarrior, we'll use fixed, already defined, slots.


Initialise the Mechwarrior slot

We create the function to initialise the slot:

void InitSlotMechwarrior(void)
{

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

          GET.pSlot->pProcInitialise = InitialiseMech;
          GET.pSlot->pProcCollision = CollisionMech;
          GET.pSlot->pProcControl = ControlMech;
          GET.pSlot->pProcDraw = (void *) 0x44f600; // default draw procedure
          // and we set as NULL the others:
          GET.pSlot->pProcCeiling = NULL;
          GET.pSlot->pProcDrawExtras = NULL;
          GET.pSlot->pProcFloor = NULL;


          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 |
                                                            enumFSLOT.MOVED_BY_ANIMATIONS;
}

Please note that the code is very alike than InitSlotCrane() but for mechwarrior there is the enumFSLOT.MOVED_BY_ANIMATIONS flag, since the speed values in animation record will affect the movement, while with other our objects, crane, SW robot and robot cleaner, it was our code to change the coordinates of the object to move it.

Note: we have also added the clearing of not used function pointers for safety:

          // and we set as NULL the others:
          GET.pSlot->pProcCeiling = NULL;
          GET.pSlot->pProcDrawExtras = NULL;
          GET.pSlot->pProcFloor = NULL;

Then we place a call to above InitSlotMechwarrior() function, in the code of cbInitObjects() function:

void cbInitObjects(void)
{
          InitSlotRobotCleaner();
          InitSlotRobotStarWars();
          InitSlotCrane();
          InitSlotCtrlPanelCrane();
          InitSlotMechwarrior();
}


And at end, we'll create the function for mech management, with same names we have already written in the slot structure:

void InitialiseMech(short ItemIndex)
{

}

void ControlMech(short ItemIndex)
{

}

void CollisionMech(short ItemIndex, StrItemTr4 *pLara, StrCollisionLara *pLaraCollision)
{

}



Initialise Lara's animation slot of Mechwarrior

As explained, in spite there will be no default functions for this slot, own for this reason we should set with NULL all the pointers for this function in its slot, to avoid wasting time, performing old function of slot used to host the lara'a animations.

void InitSlotMechwarriorLara(void)
{
          // get slot structure with Mech:
          Get(enumGET.SLOT, enumSLOT.MECH_WARRIOR_LARA , 0);
          if ((GET.pSlot->Flags & enumFSLOT.PRESENT) == 0) {
                    // the flag PRESENT is missing, so there is NO Mech in this level: we quit immediatly the code
                    return;
          }
          // clear all default function for this slot:
          GET.pSlot->pProcCeiling = NULL;
          GET.pSlot->pProcCollision = NULL;
          GET.pSlot->pProcControl = NULL;
          GET.pSlot->pProcDraw = NULL;
          GET.pSlot->pProcDrawExtras = NULL;
          GET.pSlot->pProcFloor = NULL;
          GET.pSlot->pProcInitialise = NULL;
}



Now we have to call our InitSlotMechwarriorLara() function, from the code of usual cbInitObjects() callback function:

void cbInitObjects(void)
{
          InitSlotRobotCleaner();
          InitSlotRobotStarWars();
          InitSlotCrane();
          InitSlotCtrlPanelCrane();
          InitSlotMechwarrior();
          InitSlotMechwarriorLara();
}


The InitialiseMech() function

Now we type the code for InitialiseMech() function.
Please do not confuse the initialise function for SLOT with that for ITEM.
In above chapter we initialised the slot of mechwarrior: a structure where there are general settings for all object of that kind.
While now we build the code to initialise this single moveable item.
In InitialiseItem() we'll set the current animation and state-id for the moveable, and we'll initialise also the global fields "Reserved_34/36/38/3A", of its structure.
For the mechwarrior the beginning animation it will be 30: that where the mech is still to wait lara goes in.

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

          // set state-id:
          GET.pItem->StateIdCurrent = 2;
          GET.pItem->StateIdNext = 2;

          // get slot structure of mech:
          Get(enumGET.SLOT, GET.pItem->SlotID ,0);
          // set animation at 30
          GET.pItem->AnimationNow = GET.pSlot->IndexFirstAnim+30;

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

          // set current first frame for current animation:
          GET.pItem->FrameNow = GET.pAnimation->FrameStart;

          // clear (initialise) its global fields:
          GET.pItem->Reserved_34 = 0;
          GET.pItem->Reserved_36 = 0;
          GET.pItem->Reserved_38 = 0;
          GET.pItem->Reserved_3A = 0;

          // set flags:
          GET.pItem->FlagsMain = enumFITEM.NOT_YET_ENABLED;
          // get active the item
          AddActiveItem(ItemIndex);
}

In above code we can see a weirdness: we set the enumFITEM.NOT_YET_ENABLED flag, and then we call the AddActiveItem() functon to enable it.
Really it is this the right procedure. If we omit the enumFITEM.NOT_YET_ENABLED flag, the AddActiveItem() function should have not worked fine.
At end of these two instructions we got that the mechwarrior was enabled lke it had been triggered in game.


The CollisionMech() function

We type also the basic code for collision procedure of mechwarrior:

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

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

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


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

          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);
}

Then, we'll have to add other code to handle the get-in animation, but now we made only that is necessary to get that mechwarrior had its collision box working.


The ControlMech() function

Also for this function now we type only the minimal required code: the call to AnimateItem() function to do apply the current animations to the object.
Then we'll add many other code

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

          AnimateItem(GET.pItem);
}


Since we are working on the script, we can add to [Options] section of the script, the LogItem script command to discover ideal position of Lara with mechwarrior:

LogItem= FLI_SHOW_DIFFERENCES, 153 ;153=index of mechwarrior



Discover ideal position for get-in animation on Mechwarrior

Now we build the library and try in game to find data for ideal position of lara respect the mechwarrior.

Looking logitem data we have following (rounded) ideal values:
DifX = 0
DifY = 0
DifZ = 429
HOrientDiff = $8000
VOrientDiff = 0
ROrientDiff = 0

Now we type these values, with an tollerance range in a StrTestPositionCmd structure, like we did for testposition of control panel.
So we declare at top of soure and outside of all function a new global variable for mechwarrior alignment:

StrTestPositionCmd CtrlCraneTestPosition;

StrTestPositionCmd MechWarriorTestPosition;


Then in InitializeAll() function we set all values in this variable:

          // initialise data for testposition for mechwarrior
          MechWarriorTestPosition.Slot = enumSLOT.MECH_WARRIOR;

          pPos = &MechWarriorTestPosition.DatiPosition;

          pPos->Distance.MinX = 0 - 200;
          pPos->Distance.MaxX = 0 + 200;
          pPos->Distance.MinY = 0 - 100;
          pPos->Distance.MaxY = 0 + 100;
          pPos->Distance.MinZ = 429 - 200;
          pPos->Distance.MaxZ = 429 + 200;

          pPos->Orienting.OrientHMin = (short) 0x8000 - 0x1000;
          pPos->Orienting.OrientHMax = (short) 0x8000 + 0x1000;
          pPos->Orienting.OrientVMin = 0;
          pPos->Orienting.OrientVMax = 0;
          pPos->Orienting.OrientRMin = 0;
          pPos->Orienting.OrientRMax = 0;

          MechWarriorTestPosition.Flags =TPOS_TURN_FACING_180;

Note: I had to use the TPOS_TURN_FACING_180 flag, because mechwarrior and lara have a difference about facing of 0x8000 and in this case (to avoid, when it is possible) there are always problems with alignment, so it's necessary using some TPOS_ flag to fix the problems.
As general rule, when you build a vehicle it should be better if its facing was chosen to avoid that in get-in animation lara and the vehicle had a difference of facing of 0x8000, like it happens with mechwarrior.

Now we have all right values stored in MechWarriorTestPosition structure, so we can add to the code of CollisionMech() function, the code to detect if lara is in ideal position respect the mechwarrior.
The code is almost the same used for control panel of crane:

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

          if (GetMaxDistance(&GET.pItem->CordX, &pLara->CordX, false) > 1600) return;
          Get(enumGET.INPUT,0,0);
          if (GET.Input.GameCommandsRead & enumCMD.ACTION) {

                    // it has been sent ACTION command
                    if (pLara->StateIdCurrent == 2 && pLara->AnimationNow == 103) {

                              // lara is in stand-up position (stateid=2 animation=103)
                              Get(enumGET.INFO_LARA,0,0);
                              if (GET.LaraInfo.TestFreeHands == true) {
                                        // lara has free hands
                              
                                        if (CheckPositionAlignment(&MechWarriorTestPosition, ItemIndex)== true) {
                                                  // flash screen with red light
                                                  PerformFlipeffect(NULL, 355, 0, 10);
                                        }
                              }
                    }
          }

Like in the past, we begin only with a test: when lara is in right position and player hit ACTION command, there will be a red flash on the screen.
In this way we can verify if tollerance range are good or we need to change them a bit.


The CollisionMech() function

Since the test worked fine, we can complete our code like we did with control panel of the crane.
We'll use also the AlignLaraAtPosition() function to do move lara in right position. Until the alignment is not complete we'll continue to call this function.

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

          // if lara is already on this vehicle: quit all collision code
          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle == ItemIndex) return;

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

          // if there is alignment in progress on mech warrior: call immediately AlignLaraAtPosition()
          if (Trng.pGlobTomb4->TestAlignmentInProgress == true && *Trng.pGlobTomb4->pAdr->pObjectActive == ItemIndex) {

                    if (AlignLaraAtPosition(&MechWarriorTestPosition, ItemIndex) == false) return;

                    // lara is aligned..
                    // engage the get-in animation
                    Mech_GetIn(ItemIndex);
                    return;
          }
          Get(enumGET.INPUT,0,0);
          if (GET.Input.GameCommandsRead & enumCMD.ACTION) {

                    // it has been sent ACTION command
                    if (pLara->StateIdCurrent == 2 && pLara->AnimationNow == 103) {

                              // lara is in stand-up position (stateid=2 animation=103)
                              Get(enumGET.INFO_LARA,0,0);
                              if (GET.LaraInfo.TestFreeHands == true) {

                                        // lara has free hands
                              
                                        if (CheckPositionAlignment(&MechWarriorTestPosition, ItemIndex)== true) {
                                                  // lara is about in front of mechwarrior
                                                  // perform first call to AlignLaraAtPosition() function:
                                                  // only for #debug#:
                                                  // perform triggergroup=1 to enable the fixed camera while there is get-in animation
                                                  PerformFlipeffect(NULL, 371, 1, 0);

                                                  if (AlignLaraAtPosition(&MechWarriorTestPosition, ItemIndex) == false) return;
                                                  // lara is aligned..
                                                  // engage the get-in animation
                                                  Mech_GetIn(ItemIndex);
                                                  return;
                                        }
                              }
                    }
          }
          if (TestBoundCollide(GET.pItem, pLara, pLaraCollision->LaraSizeX)==false) return;

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

          ItemPushLara(GET.pItem, pLara, pLaraCollision, false, 1);
}

The code is alike than that of CollisionCtrlPanelCrane() function, as you see, anyway now we'll describe the differences:

- At begin we check if lara is already on the veichle because in this case it has no meaning check for collision between lara and the mechwarrior, since they are pasted togheter and neither check if lara is in right position to get-in since she is already driving the mech.

          // if lara is already on this vehicle: quit all collision code
          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle == ItemIndex) return;


- When the alignment it has been completed it will be called the (new) Mech_GetIn() function where we handle the first animation and the start of driving mode.

                    // lara is aligned..
                    // engage the get-in animation
                    Mech_GetIn(ItemIndex);


- Only for debug, we'll perform a triggergroup to engage a fixed camera that allows to us the chance to see better the get-in animation:

                                                  // only for #debug#:
                                                  // perform triggergroup=1 to enable the fixed camera while there is get-in animation
                                                  PerformFlipeffect(NULL, 371, 1, 0);

This check it's necessary also because it's not easy to see fine the get-in animation, since lara begins from back and then she turns on 180 degrees while the animation is in progress and this change of default camera could get problematic the view.


Looking the animation from above picture is not evident, but lara start from back and the it reachs default facing. This means that we need to invert (by 180 degrees) the facing of Lara just this animation begins.


The Mech_GetIn() function

This function works like the Crane_EngageDriving() function: it starts the driving mode.
If starts the driving mode typing the index of mechwarrior in veihcle index system variable, and the force the get-in animation for both objects: lara and the mechwarrior:

void Mech_GetIn(short ItemIndex)
{
          Get(enumGET.LARA,0,0);

          // entering in driving vehicle mode:
          *Trng.pGlobTomb4->pAdr->pVehicleIndex = ItemIndex;
          // set that lara has not free hands:
          *Trng.pGlobTomb4->pAdr->pFlagsLaraHands = enumFLH.IS_GRABBING;
          // set animation 0: where lara climb the mechwarrior and at end it close the cage and stands up
          // note: since lara is in front of mechwarrior, so we have to invert lara's facing
          GET.pLara->OrientationH += (short) 0x8000;
          // force animation 0 for mechwarrior:
          ForceAnimationForItem(GET.pItem, 0, 7);
          // and now we set same animation and coordinate for Lara:
          Mech_NormalizeCoordinate(ItemIndex);
}

We have to change immediately the facing of lara:

          GET.pLara->OrientationH += (short) 0x8000;

This operation it's not standard for vehicle, of course. It depends by this particular get-in animation.
Then we call another new function: the Mech_SetAnimations() function.
It's better using a function, rather typing everytime the code, because now everytime we change the animation for mechwarrior we'll have to change in same way also the animation for lara, so it's better having a function for this target, since we'll use very often this code.

The Mech_SetAnimations() function


// set same relative animation for lara and for mechwarrior
void Mech_SetAnimations(short ItemIndex, int NumAnim)
{
          int AbsLaraAnim;
          int AbsMechAnim;

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

          // find slot with lara's animations
          Get(enumGET.SLOT, enumSLOT.MECH_WARRIOR_LARA , 0);
          AbsLaraAnim = GET.pSlot->IndexFirstAnim + NumAnim;

          // find slot with mech's animations
          Get(enumGET.SLOT, GET.pItem->SlotID, 0);
          AbsMechAnim = GET.pSlot->IndexFirstAnim + NumAnim;

          // ---- set animation, frame and state id for MechWarrior
          Get(enumGET.ANIMATION, AbsMechAnim,0);
          GET.pItem->AnimationNow = AbsMechAnim;
          GET.pItem->FrameNow = GET.pAnimation->FrameStart;
          GET.pItem->StateIdCurrent = GET.pAnimation->StateId;
          GET.pItem->StateIdNext = GET.pAnimation->StateId;

          // ---- set animation, frame and state if for lara
          Get(enumGET.ANIMATION, AbsLaraAnim,0);
          GET.pLara->AnimationNow = AbsLaraAnim;
          GET.pLara->FrameNow = GET.pAnimation->FrameStart;
          GET.pLara->StateIdCurrent = GET.pAnimation->StateId;
          GET.pLara->StateIdNext = GET.pAnimation->StateId;
}

And at end we set for lara same coordinate of mechwarrior.
Also this code is in a new function, Mech_NormalizeCoordinate(), because we'll use often it.


The Mech_NormalizeCoordinate() function

This function, other to copy the input coordinate (x,y,z) in both objects (mechwarrior and lara) call also the updateitemroom() function to update the room in the case they changed current room in that moving.

void Mech_NormalizeCoordinate(short ItemIndex)
{
          StrItemTr4 *pMech;
          int CurrentAnim;
          int CurrentFrame;

          // to get easier, copy mech structure in a short variable:
          pMech = GET.pItem;

          // ---- set for lara same position of mechwarrior

          GET.pLara->CordX = pMech->CordX;
          GET.pLara->CordY = pMech->CordY;
          GET.pLara->CordZ = pMech->CordZ;

          // --- copy state id data ------
          GET.pLara->StateIdCurrent = pMech->StateIdCurrent;
          GET.pLara->StateIdNext = pMech->StateIdNext;

          // --- copy animation data ------
          CurrentAnim = GetRelativeAnimation(pMech);
          CurrentFrame = GetCurrentFrame(pMech);

          Get(enumGET.SLOT, enumSLOT.MECH_WARRIOR_LARA,0);
          GET.pLara->AnimationNow = GET.pSlot->IndexFirstAnim + CurrentAnim;
          Get(enumGET.ANIMATION, GET.pLara->AnimationNow, 0);

          GET.pLara->FrameNow = GET.pAnimation->FrameStart + CurrentFrame;

          // ------ copy orientation (facing) -------------------
          GET.pLara->OrientationH = pMech->OrientationH;
          GET.pLara->OrientationT = pMech->OrientationT;
          GET.pLara->OrientationV = pMech->OrientationV;

          // ------- copy speed data -------------------------------
          GET.pLara->SpeedH = pMech->SpeedH;
          GET.pLara->SpeedV = pMech->SpeedV;


          // ------- update room for mech and lara ------------------
          UpdateItemRoom(ItemIndex);
          UpdateItemRoom(GET.LaraIndex);
}

About the code of above function there is a speech to do...
My choice it has been to copy all data affected by animations, as frame, hspeed and state id, but also data about facing and vertical speed.

The advantage of this method is that now we can work on only one movable: the mechwarrior and then all changes we did on the mech will be mirrored in lara object.
This is true not only for the code, when you set an animation or change facing or vertical speed, but also in the planning of animation using animation editor program.
Since all changes in mechwarrior will be copied to lara object, it's not necessary you set in some fields of lara's animations values like: hspeed, accell, or the change state-id values, because once you set these values in the animations of mechwarrior all changes will be copied also in lara object.
For instance, if you set some state-id changes in animation 2 of mechwarrior, to do start the animation 3 when there is a given next state id, once this change it will happen in mechwarrior, its animation will become the animation 3, and then the Mech_NormalizeCoordinate() function, will force the animation 3 also for lara, in spite her animation had no state-id change record.
This is a way to get easier the code and the building of animations: you can work on only one moveable, the mechwarrior, and all changes will copied by code in lara object.


Testing get-in animation for Mechwarrior

Now we can build the plugin and try in game how it works the get-in animation.



Not so bad for first attempt.
Anyway it's not easy discover alignment problems at 30 fps, we can slow down the game to see if all animation works fine.
Type in [Option] section of the script the commands:

Diagnostic=ENABLED
DiagnosticType=DGX_ERRORS, EDGX_SLOW_MOTION

Now we repeat the test, but this time, before hitting ACTION to engage the animation, we hit many times the F11 key to slow down the game and then we hit ACTION to see what's happen with better details.


The code for get-off animation of Mechwarrior

Well, since also the test in slow-motion of get-in animation worked fine, now we can try to set the opposite animation: that to quit the driving, where lara will move off from the mechwarrior.
Also this is a delicate phase, since we should divide our vehicle in two objects and to do coming back lara at her common working mode.

Animations to handle get-in and get-off code

Anim Next StateId Description
0 2 7 Get-In animation. Lara gets in the mechwarrior.
1 30 3 Get-off animation. Lara gets off the mechwarrior.
2 2 1 Idle animation. Lara is on board, the mechwarrior is still in stand-up position.
30 30 3 Quit driving. Lara is on the floor in stand-up position, waiting the quit of driving.


The animation to get-off is the number 1, while the idle animation, when lara is on board but the mechwarrior is still, is the number 2.
To engage this get-off animation we should type also some code to get game input.
We could use ROLL command to quit driving since it is the same used for manual crane and it is also (usually) the "end" key on keyboard.
We'll type this code in ControlMech() function, of course.



The ControlMech() function

void ControlMech(short ItemIndex)
{
          Get(enumGET.ITEM, ItemIndex,0);
          Get(enumGET.LARA,0,0);
          AnimateItem(GET.pItem);

          // if lara is not driving the mechwarrior, there is nothing other to do
          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle != ItemIndex) return;

          // lara is driving the mech warrior
          // remember to animate lara
          AnimateItem(GET.pLara);

          Mech_InputCommands(ItemIndex);

          if (Mech_UpdateAnimations(ItemIndex)==true) return;
}

This time we have already divided the main control procedure in some logic units (sub-functions).

If lara is not driving this mechwarrior we can quit the code after having performed only the AnimateItem() function:

          // if lara is not driving the mechwarrior, there is nothing other to do
          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle != ItemIndex) return;



The Mech_InputCommands() function
In the Mech_InputCommands() function we'll handle the game input for mechwarrior:

// globals: GET.pItem is mechwarrior structure
void Mech_InputCommands(short ItemIndex)
{
          Get(enumGET.INPUT,0,0);

          if (GET.Input.GameCommandsRead & enumCMD.ROLL) {
                    // player hit ROLL(end) to quit driving: verify if there are right conditions
                    if (GET.pItem->SpeedH == 0 && GET.pItem->SpeedV == 0 && GetRelativeAnimation(GET.pItem) == 2) {
                              // mechwarrior is still with idle animation 2: force get-off animation 1

                              Mech_SetAnimations(ItemIndex, 1);
                              // only for #debug#:
                              // perform triggergroup=1 to enable the fixed camera while there is get-off animation
                              PerformFlipeffect(NULL, 371, 1, 0);
                    }
          }
}

We check for ROLL command but to accept it, it's necessary also the mechwarrior was in right situation: no moving and animation 2 (idle).
We used newly the trick to enable a camera to see from side the getting off animation, to have a better view of this phase.


The Mech_UpdateAnimations() function
While in the Mech_UpdateAnimations() function we'll type the code to check current animation and decide further actions in according this animation or current state id.

bool Mech_UpdateAnimations(short ItemIndex)
{
          int NAnim;

          NAnim = GetRelativeAnimation(GET.pItem);

          switch (NAnim) {

          case 30:
                    // lara is off board and wait to quit the driving
                    // call the function to quit the driving
                    Mech_QuitDriving(ItemIndex);
                    return true;
          }
          return false;
}

Currently we check only for animation 30. This animation it will be played after the get-off animation 1.
So we use this signal to quit driving, calling the Mech_QuitDriving() function.


The Mech_QuitDriving() function

void Mech_QuitDriving(short ItemIndex)
{
          // disable driving mode:
          *Trng.pGlobTomb4->pAdr->pVehicleIndex = -1;
          // set free hands for lara
          *Trng.pGlobTomb4->pAdr->pFlagsLaraHands = enumFLH.ALMOST_FREE_HANDS;
          // set for lara the animation 103
          ForceAnimationForLara(103, 2);
}



Testing the get-off animation for Mechwarrior

To perform the test it's better using the EDGX_SLOW_MOTION flag and hit f11 many times to see all animation frame for frame, slowly.




The get-off phase works fine, excepting when the driving will be quit and the 103 animation begun (picture 6 in above image).
If you compare picture 5 and 6 you see that lara seems moved back of almost one sector but the reality it's another: the coordinate of lara is always the same, the get-off animation moved the meshes of lara without changing really her object position in 3d space.
This is a typical situation to fix with a SetPosition animcommand.
Anyway since we have the chance to change everything we wish with our plugin, we can get easier typing the code to move lara forward before setting 103 animation.


How to get a set position animcommand directly whereby code

We can use the GetIncrements() function to compute the new position of lara, applying a given distance and direction (facing).
Now we have to understand what is this distance.
If you compare picture 5 and 6 of above image, the distance seems a bit less than one sector but this is only a blunder.
When we quit the driving, the collision procedure of mechwarrior comes back to work and lara will be pushed away.
To understand the real distance we have to compare the X,Z position between the picture 1, where lara meshes are yet in original position of object origin, and the final position of picture 6. This distance seems half sector, 512 game units.
Anyway now we can verify if this distance is correct.

We change the code in Mech_QuitDriving() function, moving lara forward by 512 game units before engaging the animation 103:

void Mech_QuitDriving(short ItemIndex)
{
          int IncX, IncZ;

          // disable driving mode:
          *Trng.pGlobTomb4->pAdr->pVehicleIndex = -1;
          // set free hands for lara
          *Trng.pGlobTomb4->pAdr->pFlagsLaraHands = enumFLH.ALMOST_FREE_HANDS;

          // move lara forward (in her facing) by 512 game units
          GetIncrements(GET.pLara->OrientationH, &IncX, &IncZ, 512);

          GET.pLara->CordX += IncX;
          GET.pLara->CordZ += IncZ;
          UpdateItemRoom(GET.LaraIndex);

          // set for lara the animation 103
          ForceAnimationForLara(103, 2);
}

Now we verify if this distance is right...

Testing fixing for get-off animation




It works! Second and third picture of above image, seem the same, but in the middle there is the animation changing between animation 30 of lara driving mode, and the animation 103 of standard lara.


How to handle camera mode for get-in animation

In previous test we used a fixed camera to look the get-in and get-off animation from a side of mechwarrior but now we have to check how the default camera works when lara gets in mechwarrior.
So, we'll remove, from Mech_InputCommands() function, the rows:

// only for #debug#:
// perform triggergroup=1 to enable the fixed camera while there is get-off animation
PerformFlipeffect(NULL, 371, 1, 0);


and from CollisionMech() function, the rows:

// only for #debug#:
// perform triggergroup=1 to enable the fixed camera while there is get-in animation
PerformFlipeffect(NULL, 371, 1, 0);


In this way only default camera will be used.

Now we verify how default camera works when lara gets in mechwarrior:




Results are not good.
Before engaging the get-in animation the view is good, but once the get-in animation started the camera moved back the mechwarrior and the scene while lara is climbing the mech it is neither visible.
Only at end of get-in animation, when it starts the idle animation 2, the view is acceptable.

We should change default camera to look lara from a better view.


The SetCamera() function


void SetCamera(int Distance, int HOrient, int VOrient, int Speed)

There are many trng triggers to change camera mode, like also common triggers can enable a fixed camera placed in the map, anyway the SetCamera() has some differences by all above methods:
  1. This camera doesn't require any camera object in the level. It works also if in whole level there is no fixed camera

  2. This camera mode has a very short persistence. Indeed it works only for current frame

  3. It works only to look Lara, indeed it affects only the angle and distance of default chase camera of Lara


The SetCamera() uses the same method used by tomb raider engine to change temporary the way to look lara when she is performing some special animation or state-id, like she is climbining or monkeing.

The durate of this view is only for current frame, for this reason you should continue to call the function with same input parameters, until you wish keeping this camera mode.
While when you wish coming back to default chase camera, just simply you don't call SetCamera() function, and in few frames the camera mode will come back to default view.

About the meaning of HOrient and VOrient see the full description of this function in Function Collection help file
Now just knowing that HOrient changes the position of camera around of Lara in horizontal, while VOrient in vertical and the Speed value is the number of frames required to change from old position to current position the camera. If you set 1, the change of position it will be immediate.

To fix our problem we should set parameters to see lara's from back also with animation 0, in spite in this animation lara gives the back to camera.
Since we have to call continuosly SetCamera() for all time of animation 0, we could place the code in Mech_UpdateAnimations() function:

bool Mech_UpdateAnimations(short ItemIndex)
{
          int NAnim;

          NAnim = GetRelativeAnimation(GET.pItem);

          switch (NAnim) {
          case 0:
                    // get-in animation: change camera to see lara from back in spite the meshes are turned by 180 degrees
                    SetCamera(-1, 0x8000, -1, 30);
                    break;

We added a "case 0:" to handle when animation 0 is in progress and we changed the horizontal view to see lara
We typed "-1" for Distance and VOrient because we are not interested to change these values.

Now we try in game what happens...


New camera setting for get-in Animation



It works fine, in particular way it's important that it was not visible the change of camera from standard with animation 103 and following from forward with animation 0, because our change compensate in precise way the change of lara's facing in right moment.
At end of animation 0 it starts the animation 2 and the camera comes back to default values. You see in D picture the view of default camera.
It's acceptable but not so good. We should understand that camera worked for lara but now lara has higher distance from floor and the mechwarrior is bigger than lara, so it should be better that the chase camera look lara from higher position and it was a bit more far than default value.
We can try to apply these new settings when mechwarrior is performing animation 2, the idle animation.

bool Mech_UpdateAnimations(short ItemIndex)
{
          int NAnim;

          NAnim = GetRelativeAnimation(GET.pItem);

          switch (NAnim) {
          case 0:
                    // get-in animation: change camera to see lara from back in spite the meshes are turned by 180 degrees
                    SetCamera(-1, 0x8000, -1,30);
                    break;
          case 2:
                    // idle animation: lara on board and mechwarrior still
                    SetCamera(2200, -1, -7000, 1);
                    break;

We moved camera to be more far and heigher.
Now we see in game how it works.




At left you see the default camera while at right the new camera set for the idle animation using the code:

                    SetCamera(2200, -1, -7000, 1);

It could seem weird but it was necessary that player was able to see where the mechwarrior is placing its feet.
With default camera the sector where the mech was standing it was not visible.
By other hand, getting the view of current sector, moving yet more far the camera was problematic because it could remain hidden behind obstacle if it is too far by the mech,


Set permanently the new camera mode

We did the experiment with idle animation 2, but the camera mode for mech should be stable and work for all driving time.
So we should change the code to have always that setcamera, with only exception of get-in animation.
So we change the code of Mech_UpdateAnimations() function in this way:

bool Mech_UpdateAnimations(short ItemIndex)
{
          int NAnim;

          NAnim = GetRelativeAnimation(GET.pItem);

          // set new camera mode for mech:
          SetCamera(2200, -1, -7000, 1);

          switch (NAnim) {
          case 0:
                    // get-in animation: change camera to see lara from back in spite the meshes are turned by 180 degrees
                    SetCamera(-1, 0x8000, -1, 30);
                    break;
          case 30:
                    // lara is off board and wait to quit the driving
                    // call the function to quit the driving
                    Mech_QuitDriving(ItemIndex);
                    return true;
          }
          return false;
}

We set in advance, as new default camera, the call to SetCamera() to have the view we have just seen for idle animation 2:

          // set new camera mode for mech:
          SetCamera(2200, -1, -7000, 1);

Then, in the switch() for current animation, we change newly the data of setcamere in the case 0 for get-in animation:

          switch (NAnim) {

          case 0:
                    // get-in animation: change camera to see lara from back in spite the meshes are turned by 180 degrees
                    SetCamera(-1, 0x8000, -1, 30);
                    break;

This double setting is not a problem, because when you call SetCamera() function, nothing happens immediately. Only some critical variables of tomb raider engine have been changed, but their values will be used only when it will be handled the camera position. So we can change two or many times the data in same function with no risk of flickering on the screen.


The camera mode for get-off animation

So now we'll have a camera mode for all time that lara is driving the mechwarrior. Only one exception is that for get-in animation, to give the chance to see she is climbing the vehicle.
And, about the get-off animation?
Do you wish keep really the same camera used for driving also for when she is leaving the mechwarrior?

Look fine what the player will see when there is the get-off animation...




For almost whole get-off animation, lara is not visible (B and C images), other to be not well visible when we came back to default chase camera.

Perhaps it's better setting a different camera mode when lara is getting off mechwarrior...

We can add another "case" for animation 1: the get-off animation:

          // set new camera mode for mech:
          SetCamera(2200, -1, -7000, 1);

          switch (NAnim) {
          case 0:
                    // get-in animation: change camera to see lara from back in spite the meshes are turned by 180 degrees
                    SetCamera(-1, 0x8000, -1,60);
                    break;

          case 1:
                    // get-off animation
                    SetCamera(1500, 0x8000, 0, 1);
                    break;

For get-off animation set an immediate speed (1) because the position of camera was too different to wait the camera moved slowly from back to front of mechwarrior.

Now we try new camera for get-off animation...




Now it's better, at least we see lara while she climbs down.
About last image C, it's not possible avoiding that bad framing, since now it is the default camera to work and the driving of mechwarrior quits.


Animations to move forward

Since mechwarrior has a lot of animations, 30 but they could growing, I prefer show only the group of animations we are going to apply in our code, rather showing all immediately.

Animation to move forward MechWarrior

NAnim NextAnim StateId Description
2 2 1 Idle Animation. Mechwarrior is still.
3 4 4 Start Moving Forward Animation. Mech perform first step forward.
Speed 16
4 4 4 Run forward Animation. Looped animation with mech walking forward.
Speed 32
5 2 6 Stop animation rigth foot. Mech pass from run to idle animation while it had right foot forward
6 2 6 Stop animation left foot. Mech pass from run to idle while it had left foot forward


Our next step it will be to do move forward the mechwarrior when we receive the UP input command.
We'll have to check if there is free space in front of the mechwarrior, and then set right animation.
If the mech is already moving forward, but it finds an obstacle we'll have to stop it, setting next state id 6 that it will force the stop animation and then the idle animation. See above table

Respect to other objects, the big difference is that it will be the AnimateItem() function to update the position of the mechwarrior, applying the hspeed value in specific current animation.
This situation will change our code respect the past.
For instance a first change we should do, it's to place the call to AnimateItem() function, at end of ControlMech() function, while now it is at begin.
The reason is that we have to check if mechwarrior is going to bump into wall first that it happens, and when we discover this risk, we should change animation to stop mechwarrior.
So we'll restyle our ControlMech() function in this way:

void ControlMech(short ItemIndex)
{
          Get(enumGET.ITEM, ItemIndex,0);
          Get(enumGET.LARA,0,0);
          // if lara is not driving the mechwarrior, there is nothing other to do
          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle != ItemIndex) {
                    AnimateItem(GET.pItem);
                    return;
          }
          Mech_InputCommands(ItemIndex);

          if (Mech_UpdateAnimations(ItemIndex)==true) return;
          // normalize data copying from mech to lara
          Mech_NormalizeCoordinate(ItemIndex);
          // update animations
          AnimateItem(GET.pLara);
          AnimateItem(GET.pItem);
          // normalize newly the coordinate, anim, frame ect, because AnimateItem could have changed something in

          // mechwarrior strucutre and we need to have same change also in lara structure
          Mech_NormalizeCoordinate(ItemIndex);
}

We placed at end of function the calls of AnimateItem() for lara and mechwarrior, while we use another AnimateItem() call when driving mode is not yet active, to get the animations of mechwarrior were anyway applied:


          // if lara is not driving the mechwarrior, there is nothing other to do
          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle != ItemIndex) {
                    AnimateItem(GET.pItem);
                    return;
          }


The computation about collision for big objects

How we said, another news we have to handle with mechwarrior is its big size.
We cann't work like we did with Robot Cleaner or SW robot, checking only one sector to discover if there is free way, because mech with its big size could collide with its arms also outside of a single sector.
In these situations we have to do multiple checks on its boundary collision box to discover what are the values about floor and ceiling for all these check points.



In this image you see the collision box of mechwarrior.
We should check at least three point of its collision box, you see them as red points. We'll place these three coordinate in an array (a vector) to get easier perform in a loop same computation on all three points with same code.
About how to locate these three points, we'll use the GetIncrements() function with a little trick.
I mean that, while for the point in front of x,y,z pivot it's easy, because just passing to GetIncrements() function the direction (facing of mechwarrior), the distance and we'll get IncX and IncZ to add to mech origin (pivot) to get that point (that in left image it has been labelled as VetPoints[1])
But to discover other two points, on the corners, how could we do?
The trick is to use newly the GetIncrements() fucntion but this time we'll use as starting point not the pivot of the robot, but the first point we have just discovered, applying on it a direction that was changed by 90 degrees at left, to get VetPoints[0]), and at right to get VetPoints[2] and as distance the value you see labeled as "HalfWidth" since it is the half of width of collision box.
Once we have these three points, we'll use them with CheckFloor() function to see the height of floor and ceiling and then we'll try to discover the type of floor that the mechwarrior has in front of it.
Since the check it will be about three different points, we have to set a coded returned vale, that it was able to describe some generic situation about collision.
So we create new mnemonic constants, with CRC_ prefic (CRC= Check Room Collisions) and then we'll use this value to choose what animations perform or what are the chances for mechwarrior to move or less.



The constants to identify collision type

We create these new constant to describe different collision types:

#define CRC_OK 0x0001 // mech is able to move with no change
#define CRC_TWO_CLICK_UPSTAIRS 0x0002 // mech requires to use animation to climb 2 click obstacle
#define CRC_TWO_CLICK_DOWNSTAIRS 0x0004 // mech requires to use animation to climb down for 2 click space
#define CRC_SLIDE_AT_RIGHT 0x0008 // steep slope, mech should roll/sliding at right
#define CRC_SLIDE_AT_LEFT 0x0010 // steep slope, mech should roll/sliding at left
#define CRC_STEEP_GORGE 0x0020 // higher than 2 click
#define CRC_STOP 0x0040 // it's not possible moving in this position

#define CRC_COLLIDE_LEFT 0x0080 // mech is colliding with left side
#define CRC_COLLIDE_MIDDLE 0x0100 // mech is colliding with middle side
#define CRC_COLLIDE_RIGHT 0x0200 // mech is colliding with right side

#define CRC_FALLING_DOWN 0x0400 // mech is over the empty: required to enable gravity
#define CRC_FALLING_AT_RIGHT 0x0800 // only left foot is on the ground, while the right foot is in the empty
#define CRC_FALLING_AT_LEFT 0x1000 // only right foor is on the ground, while the left foot is in the empty

#define CRC_FALLING_STOP 0x2000 // mech touch the ground after falling down
#define CRC_FALLING_BACKWARD 0x4000 // mech is falling backward


We set two values for next obstacle by 2 clicks heights, because mechwarrior is able to climb over two clicks higher floor with specific animations.
About the meaning of these values you can see following images that show all different CRC_ values.




While about the CRC_STOP value, it may be affected by different situations, anyway the logic is that it's enought that or ceiling is too low, or some check point of floor is too heigh (more than 2 clicks) to have the CRC_STOP returned.


In above picture, at left you see a too heigh floor, while at right a too low ceiling to stop the mechwarrior.

About the CRC_FALLING..., they will work when mech warrior is going to fall down, and we have different kinds of falling to have a realistic simultation about the way whereby it will fall.


While the CRC_FALLING_DOWN and CRC_FALLING_STOP are generic flags to signal any falling phase or to reach the end of falling phase.


The Mech_CheckRoomCollisions() function

Now we type the code to analyse the floor in front fo mechwarrior that it will return a CRC_ value to describe what it has discovered.

// check if 3 points: one in front and other two on corners at HalfWidht of first point there is
// space to host moveable pObject
// returns a CRC_ (CheckRoomCollison) value to describe the type of collision found
// note: in GET.pItem there is mechwarrior structure
int Mech_CheckRoomCollisions(short ItemIndex, short Distance, int HalfWidth)
{
          int CordX;
          int CordZ;
          int IncX;
          int IncZ;
          int TopY;
          int Border;
          StrItemTr4 *pObject;
          int i;
          StrTriplePoint VetPoints[3];
          bool TestAll;
          int BaseY;

          // salva structure of mech
          pObject = GET.pItem;

          Get(enumGET.ITEM_COLL_BOX, ItemIndex,0);

          // discover top y coordinate of mech:
          TopY = pObject->CordY + GET.pCollItem->MinY;

          // discover the distance between pivot and border in side of current direction
          
          Border = GET.pCollItem->MaxZ;

          // add to distance the distance of border
          Distance += Border;

          // discover first point: placed at Distance in HOrient direction respect pivot of item
          // this is middle point of mech side we are checking
          GetIncrements(pObject->OrientationH, &IncX, &IncZ, Distance);


          CordX = pObject->CordX + IncX;
          CordZ = pObject->CordZ + IncZ;

          // save middle point
          VetPoints[1].CordX = CordX;
          VetPoints[1].CordZ = CordZ;

          // now discover left corner
          // turning by 90 degrees at left and using HalfWidth distance
          GetIncrements(pObject->OrientationH - 0x4000, &IncX, &IncZ, HalfWidth);

          // save left corner
          VetPoints[0].CordX = CordX + IncX;
          VetPoints[0].CordZ = CordZ + IncZ;

          // compute the right corner
          GetIncrements(pObject->OrientationH + 0x4000, &IncX, &IncZ, HalfWidth);

          // save right corner
          VetPoints[2].CordX = CordX + IncX;
          VetPoints[2].CordZ = CordZ + IncZ;

          // now check all three points:
          // if ceiling is too low, quit the function returning CRC_STOP
          // while if ceiling is ok, save the height of floor in that point
          for (i=0;i<3;i++) {
                    // analyse all three point one for one
                    CordX = VetPoints[i].CordX;
                    CordZ = VetPoints[i].CordZ;
                    
                    CheckFloor(CordX, pObject->CordY - 256, CordZ, pObject->Room);

                    if (FLOOR.CeilingHeight == WALL_FLOOR) return CRC_STOP;

                    if (TopY < FLOOR.CeilingHeight) return CRC_STOP;

                    if (FLOOR.FloorHeight == WALL_FLOOR) return CRC_STOP;

                    // save floor height in current point
                    VetPoints[i].CordY = FLOOR.FloorHeight;
          }

          // now we have the heigth of all three points.
          // set a kind of collision verifying different conditions:

          // if all three point have a diffrence respect current Y coordinate of mech, less than 400 game
          // units (less than 2 click) we return CRC_OK because mech will be able to go on with no problem
          TestAll=true;
          for (i=0;i<3;i++) {
                    if (AbsDiffY(pObject->CordY, VetPoints[i].CordY) > 400) TestAll=false;
          }

          if (TestAll == true) {
                    // all three points have a difference less than 400 game unit: ok
                    return CRC_OK;
          }
          // if at least one point is heigher than 2 click (512 game units but we round to 600) respect current Y coordiante of mech
          // then set CRC_STOP
          BaseY = pObject->CordY - 600;
          for (i=0;i<3;i++) {
                    if (BaseY > VetPoints[i].CordY) {
                              // there is an obstacle too heigh
                              return CRC_STOP;
                    }
          }
          // if all points are lower respect y coordinate of mech by 600 game units: there is a gorge: return CRC_STEEP_GORGE
          TestAll=true;
          BaseY = pObject->CordY + 600;
          for (i=0;i<3;i++) {
                    if (BaseY > VetPoints[i].CordY) {
                              // no, this point is not so far from Y mech
                              TestAll=false;
                              break;
                    }
          }
          if (TestAll == true) {
                    // all points are 600 game units (or more) lower than Y mech coordinate
                    return CRC_STEEP_GORGE;
          }
          // if there is a large height difference between the two opposite corners set CRC_SLIDE_AT_RIGHT or CRC_SLIDE_AT_LEFT
          if (AbsDiffY(VetPoints[0].CordY, VetPoints[2].CordY) > 300) {
                    // yes: now discover if the declivity is at right or left respect the mech facing
                    if (VetPoints[0].CordY < VetPoints[2].CordY) {
                              // left point is placed upwards respect right cornet: slide at right
                              return CRC_SLIDE_AT_RIGHT;
                    }
                    // it is the right cornet to be higher: slide at left
                    return CRC_SLIDE_AT_LEFT;
          }
          // if all three point are upper of Y mech of about two clicks (512 game units), return CRC_TWO_CLICK_UPSTAIRS
          TestAll=true;
          BaseY = pObject->CordY - 512;

          for (i=0;i<3;i++) {
                    if (AbsDiffY(BaseY, VetPoints[i].CordY) > 200) TestAll=false;
          }
          if (TestAll==true) {
                    // all floor is about 2 click heigher
                    return CRC_TWO_CLICK_UPSTAIRS;
          }
          // if all three points are lower than Y mech by about two clicks (512 game units), return CRC_TWO_CLICK_DOWNSTAIRS
          TestAll=true;
          BaseY = pObject->CordY + 512;

          for (i=0;i<3;i++) {
                    if (AbsDiffY(BaseY, VetPoints[i].CordY) > 200) TestAll=false;
          }
          if (TestAll==true) {
                    // all floor is lower by about 2 clicks
                    return CRC_TWO_CLICK_DOWNSTAIRS;
          }
          // it's weird reach this point of the code, anyway now we set CRC_OK then we'll discover if we missed some chance
          return CRC_OK;
}

To understand fine the code I suggest to keep in mind the image about mechwarrior collision box and the critical points and size we used




Also the input arguments of the function are showed in above picture:

int Mech_CheckRoomCollisions(short ItemIndex, short Distance, int HalfWidth)

The distance should the point where you perform the check, and it will be different in according with current speed of the mechwarrior.
Note: in the reality, in above image the Distance is given by the sum of Distance input argument + Border from pivot to its collision box limit in the direction we are checking, that is own "Distance" in above image.

While the HalfWidth value is the same you see in above image.
Theratically we can to do that value was calculated, itself, by the function, working on size of collision box, but I prefered let it as customizable input argument, because it is probable that we'll use an halfwidth littler than real, otherwise the mechwarrior it will be stopped only to touch with a side of its arms some wall border.

Once we discovered the three points, and stored their coordinates in VetPoints[] array, we apply different tests on these three points to understand if there is one of the CRC_ possible situations.

We used often the trick of "TestAll" variable to verify if some condition is true (or less) for all three y coordinate of VetPoints[] array:

          // if all three point have a diffrence respect current Y coordinate of mech, less than 400 game
          // units (less than 2 click) we return CRC_OK because mech will be able to go on with no problem
          TestAll=true;
          for (i=0;i<3;i++) {
                    if (AbsDiffY(pObject->CordY, VetPoints[i].CordY) > 400) TestAll=false;
          }
          if (TestAll == true) {
                    // all three points have a difference less than 400 game unit: ok
                    return CRC_OK;
          }

We set as true TestAll:

          TestAll=true;

Then we use a for() statement to apply the code in bracket parhenthesis for all three points:

          for (i=0;i<3;i++) {

Then we apply a condition, and when it is false we set "false" also "TestAll":

                    if (AbsDiffY(pObject->CordY, VetPoints[i].CordY) > 400) TestAll=false;

At end, we verify if the condition was true for all three coordinate, testing the value of TestAll:

          if (TestAll == true) {
                    // all three points have a difference less than 400 game unit: ok
                    return CRC_OK;
          }


The code to do move forward the Mechwarrior

Now we'll add the code to realize this little target:
  1. Detect if there is input command to do move forward: the UP command
  2. When there is, we set as next-state id the value 4, to force the animations to do move forward the mech, when there will be right frame in current animation
  3. We check if there are collision in front with Mech_CheckRoomCollisions() function
  4. If there are, then we force the next state id to do stop the mechwarrior, setting next state id 6

As we saw, with this object we don't use the ForceAnimationForItem() but we change only the next state id, waiting that the AnimateItem() function will apply the change of animation in right moment.
In spite is not the target of this tutorial, it's perhaps useful remember how the State-IDs change, works...

The State Id change records of animations

Each animation can having one or more state-id change record. They are used to pass from current animation to some other animation in right moment, when the frame of current animation is very alike than that of new animation.
To understand because it is useful we can imagine what happens when we do not use the state-id records but we change in direct way the current animation.
Look the image at left: let's say mechwarrior is running with current animation = 4.
When the mech is going to collide with a wall we should stop the running animation 4, and perform the idle animation 2, where the mech is still.
If we use the ForceAnimationForItem() function, the animation will be immediately replaced. If the mech was performing the frame 0 of animation 4, when you force the animation 2 there will be a strong jerk, because you see from left picture like the frame 0 of anim4 is different by animation 2 (almost all frames are the same, in animation 2).
We could be more lucky, of course, if when we force animation 2 the mech was performing the frame 34 of animation4, the jerk in game will be a bit less evident.
But if we use the state-id change records, we will have no need of luck!
We can say to tomb raider engine: while you are performing current animation 4, pass to other animation 2 only when the frame of animation 4 is this... or inside of following range: LowFrame HighFrame.
So in state-id record we'll type these values:
LowFrame HighFrame NextAnimation
When the frame of current animation (where we saved this state-id record) it is inside of LowFrame-HighFrame range, the NextAnimation animation it will be perfomed.
But we need of another field because otherwise the current animation (4 in our example) will pass always to idle animation and it will be no possible to do run for a long time the mechwarrior.
So we need of another field that worked like a signal about WHEN applying this state-id record.
This signal is a value named "State-Id". It is a constant value, of course, but it will be compared with the field "StateIdNext" of moveable structure for this animation.
When the "StateIdNext" of moveable is different than that of State-id record, nothing will happen. All data in state-id record will be ignored. But when the code will write in StateIdNext of moveable a value that is the same of some State-id record, the AnimateItem() function will try to launch new animation "Next Animation".
In the record so we have also a "State id" field to remember when to use that data of that state-id record.
StateId LowFrame HighFrame NextAnimation
Really there is also another field and it is to set about form what frame to perform the NextAnimation. Usually it will be 0, to perform NextAnimation from the begin, but we could also perform the nextanimation beginning from some other of its frames.
So the fields of State-id record are:
StateId LowFrame HighFrame NextAnimation NextFrame
But what happens when there is no frame of current animation that was very alike of that the animation we wish perform?
A trick is to create a new animation only to pass from some frame of current animation, to a final frame that was ideal to begin our target animation.
Psiko (the author of Mechwarrior's animations) did this work very fine. He made the animation 5, where the first frame (0) is exaclty the same of frame 0 of animation 4, while the last frame of animation 5 is the same of frames of idle animation 2.
Since the animation 5 has as next animation own the animation 2, just create a state-id record where when in animation 4 there is frame 0, the next animation will be the animation 5.



In above picture (first row) you see the state-id record of animation 4.
When in moveable will be written the value "6" as StateIdNext, if the frame of current animation 4 will be the frame 0 or 1, it will be performed the animation 5.
About second row, it is about the animation 6 that is another swap animation to perform the idle animation, but in this case the first frame of animaton 6 is the same 18 - 19 of animation 4.
Pisko created two swap animations to reduce the time required to reach a good frame to pass to idle animation. In this way the current animation 4 it will reach more fastly or one frame (0-1) or other frame (18-19).
Well done.


Changes in Mech_InputCommands() function

We add the code to detect the UP command and set the next state id at 4, because in the State-id record of idle animation 2 there is a record with state id 4 to do engage the animation 3 and then this animation 3 has as next animation the animation 4 of running.

          // ----------- UP : move forward ? --------------------------------
          if (GET.Input.GameCommandsRead & enumCMD.UP) {
                    // set as Next State id 4
                    GET.pItem->StateIdNext = 4;
          }



New ControlMech() function


void ControlMech(short ItemIndex)
{

          int TypeCollision;

          Get(enumGET.ITEM, ItemIndex,0);
          Get(enumGET.LARA,0,0);
          // if lara is not driving the mechwarrior, there is nothing other to do
          Get(enumGET.INFO_LARA,0,0);
          if (GET.LaraInfo.IndexOfVehicle != ItemIndex) {
                    AnimateItem(GET.pItem);
                    return;
          }
          Mech_InputCommands(ItemIndex);

          // check for collision only if there is an horziontal speed, or there is command to begin moving
          TypeCollision = CRC_OK;
          if (GET.pItem->SpeedH != 0 || (GET.Input.GameCommandsRead & enumCMD.UP)!= 0) {
                    TypeCollision = Mech_CheckRoomCollisions(ItemIndex, 400, 600);
          }
          if (Mech_UpdateAnimations(ItemIndex, TypeCollision)==true) return;
          // normalize data copying from mech to lara
          Mech_NormalizeCoordinate(ItemIndex);
          
          // update animations
          AnimateItem(GET.pLara);
          AnimateItem(GET.pItem);
          // normalize newly the coordinate, anim, frame ect, because AnimateItem could have changed something in
          // mechwarrior strucutre and we need to have same change also in lara structure
          Mech_NormalizeCoordinate(ItemIndex);
}

There are many news...
We call the Mech_CheckRoomCollisions() function when the mechwarrior is moving to detect further collisions.
The returned CRC_ value, will be stored in a local variable, TypeCollision, and then passed to Mech_UpdateAnimations() function, because to change animations and state ids, we need to know if mech warrior is colliding or has some special CRC_ collision in front.
For this reason we had to change the Mech_UpdateAnimations() function, adding a new input parameter, to receive the type of current collision.

The new Mech_UpdateAnimations() function


bool Mech_UpdateAnimations(short ItemIndex, int TypeCollision)
{
          int NAnim;

          NAnim = GetRelativeAnimation(GET.pItem);

          // set new camera mode for mech:
          SetCamera(2200, -1, -7000, 1);

          switch (NAnim) {
          case 0:
                    // get-in animation: change camera to see lara from back in spite the meshes are turned by 180 degrees
                    SetCamera(-1, 0x8000, -1,60);
                    break;
          case 1:
                    // get-off animation
                    SetCamera(1500, 0x8000, 0, 1);
                    break;
          case 4:
                    // mech is running forward.
                    // if there is an obstacle: stop it
                    if (TypeCollision == CRC_STOP) {
                              // force as next state id 6, because it will engage the animation 5 or 6 and at end the 2 for idle
                              GET.pItem->StateIdNext = 6;
                    }
                    break;
          case 30:
                    // lara is off board and wait to quit the driving
                    // call the function to quit the driving
                    Mech_QuitDriving(ItemIndex);
                    return true;
          }
          return false;
}

We added also a "case 4" about when mechwarrior is running forward, and in that code we verify if there is a collision.
When there is, the code will set next state id 6 to require the change to idle animation 2, to stop the mechwarrior:

          case 4:
                    // mech is running forward.
                    // if there is an obstacle: stop it
                    if (TypeCollision == CRC_STOP) {
                              // force as next state id 6, because it will engage the animation 5 or 6 and at end the 2 for idle
                              GET.pItem->StateIdNext = 6;
                    }
                    break;


Testing the forward movement

Now we verify in game how the new code works...


It works enough fine: when the mech is close to the wall (C picture) the code set the idle animation and the vehicle stops (D Picture).
About D picture you see that the right arm of mechwarrior "enters" in that low floor. It seems a bug but we followed only same method used by other enemies or vehicles: using a shorter collision box to avoid that a little collision on a side, stops the movement.
That shorter size it was the value we gave for HalfWidth field of Mech_CheckRoomCollisions() function:

                    TypeCollision = Mech_CheckRoomCollisions(ItemIndex, 400, 600);

We used 600 game units as half width of mechwarrior but it is a bit larger.
We could increase that value if we wish it was stopped by that low floor (at its right in C and D pictures) but it's not sure it was a good idea.
In this way in many circustances only an empty side of its collision box will collide with some wall and in game the result will be not so good or comfortable.


Improving the management of UP command

The code works, but it is yet a bit "naive", since once player hit, only once, the UP command, the mechwarrior select the run animation and it will stop only for a collision with the wall.
We should allow to player to stop the mechwarrior when he wished.
The easier way, is to set that when player suspend to hit UP command, the mechwarrior will stop. In this way, only hitting continuosly the UP command the movement will go on.
This new management requires only a little change in code of Mech_UpdateAnimations() function:

          case 4:
                    // mech is running forward.
                    // if there is an obstacle or player is not hitting UP command: stop it
                    if (TypeCollision == CRC_STOP ||
                              (GET.Input.GameCommandsRead & enumCMD.UP)==0) {
                              // force as next state id 6, because it will engage the animation 5 or 6 and at end the 2 for idle
                              GET.pItem->StateIdNext = 6;
                    }
                    break;

We changed a bit the code to manage running animation 4: now we force the stop also when it's missing the UP command.


Extending the control for collisions

Testing the new management for UP command we find a bug:


When the mechwarrior is closed to the wall (A picture) and we send UP command (B Picture) the mechwarrior enters in the wall (C Picture)
The reason of this bug is because we performed the check for CRC_STOP only when there is animation 4 in progress:

          case 4:
                    // mech is running forward.
                    // if there is an obstacle or player is not hitting UP command: stop it
                    if (TypeCollision == CRC_STOP ||
                              (GET.Input.GameCommandsRead & enumCMD.UP)==0) {
                              // force as next state id 6, because it will engage the animation 5 or 6 and at end the 2 for idle
                              GET.pItem->StateIdNext = 6;
                    }
                    break;

Above is the code of Mech_UpdateAnimations() function where we manage all animations.
The problem is that the animation 4 is not the only one to move forward the mech.
Threre are swap animation between one main animation and another, that have a horizontal speed.
Now it's better look all animation of mechwarrior...


All Animations of Mechwarrior

All animations of Mechwarrior

Anim NextAnim StateId Description
0 2 2 Get-in animation. Lara gets in mechwarrior
1 30 3 Get-off animation. Lara gets off from the mechwarrior and the driving quits
2 2 1 Idle animation. Mech is still in stand-up position
3 4 4 Start Moving Forward Animation. Mech perform first step forward.
4 4 4 Run forward Animation. Looped animation with mech walking forward.
5 2 6 From run to idle. Begins with forward right foot
6 2 6 From run to idle. Begins with forward left foot
7 8 9 From idle to reverse
8 8 9 Reverse animation, looped
9 2 6 From reverse, right foot, to idle animation
10 2 6 From reverse, left foot, to idle animation
11 12 5 Start right turning, with right foot
12 12 5 Right turning, looped animation turning at right
13 4 4 From right turning to move newly forward
14 2 6 From right turning to idle animation from left foot
15 2 6 From right turning to idle animation. From right foot
16 17 7 Start left turning with right foot
17 17 7 Left turning, looped animation.
18 4 4 From left turning to move forward
19 2 6 From left turning to idle animation, from left foot
20 2 6 From left turning to idle, from right foot
21 2 8 Climb a 2 click heigh floor in front of mechwarrior, beginning from idle animation
22 2 9 Reverse climb down 2 clicks and then come back to idle animation
23 24 10 From running to start jump down, when it has in front a steep gorge
24 24 10 Jumping down, looped animation
25 2 6 From jumping down to idle animation. Mechwarrior touch the ground
26 2 11 Kicking animation. Mechwarrior kicks with right foot and then comes back to idle animation
27 2 12 Beating animation. Mechwarrior hits with both arms in front of it and then comes back to idle animation
28 31 13 From running to right sliding
29 29 0 Dying animation. Lara lost her life. This animation is looped but only for last frame, waiting the end of game
30 30 3 Waiting in board animation. Mech is still, in position to get in Lara
31 31 13 Right sliding, looped animation
32 2 6 From right sliding to idle animation
33 34 14 From running to left sliding
34 34 14 Left sliding, looped animation
35 2 6 From left sliding to idle animation
36 12 5 Start right turning with left foot
37 17 7 Start left turning with left foot
38 38 15 Looped turning on itself at left
39 39 16 Looped turning on itself at right
40 34 14 From running to left sliding (from other frame)
41 31 13 From running to right sliding (from other frame)
42 2 6 Finish right turning on itself
43 2 6 Finish left turning on itself



Planning the future improvements for Mechwarrior code

How we can see from above animation list, the mechwarrior is really a very complicated vehicle...
To avoid to get too long the description of different parts of the code, it's better to plan some number of improvements, to solve different problem that we can underatand in advance, and then show the right code of these improvements...


How avoid the violation of collisions in the swap animations

Looking above list you see that there are really many animations that had a forward (or turning) movement.
The method to use the state id to swap animations has one limit: there is a latency period between the decision to perform a change, from the code, and the real change in game.
In this time the object could pass across a wall.
This risk get bigger if you think that many swap animations have no direct way to pass to idle (stop) animation.
For instance the animation 3 work to pass from idle to run animation but if in this moment there is a collision, there is no state-id to do passing the current animation 3 to idle. We'll have to wai that the run animaton 4 begun, and only now we have another swap animation to come back to idle animation.
To avoid this problem the only one sure solution is to save the original coordinates of mechwarrior before calling AnimateItem() function (where the animation could change the position of the mech, applying some horizontal speed) and then restore these original coordinates in mechwarrior, everytime it had been detected a collision.


How to handle the floor collisions different than CRC_OK and CRC_STOP

In previous release of the code, we consider CRC_STOP as only one collision to stop the mechwarrior.
But for all other CRC_ values how should we behave?
When there are some obstacles, like CRC_TWO_CLICK_UPSTAIRS the mech is able to go on, but changing its animation.
So, since it's not possible move on with current running animation we should anyway stop the mechwarrior to come back to idle animation and only from idle animation there will be the chance to engage right animation to pass over this obstacle.
Therefor we'll have to stop run or turning animations also when we find a floor like CRC_TWO_CLICK_UPSTAIRS.


Next floor or current floor?

The Mech_CheckRoomCollisions() function checks the collision where the mech is going to arrive and not those below its current position.
This is a problem for some kind of floor, because, for instance, the CRC_SLIDE_AT_RIGHT and CRC_SLIDE_AT_LEFT but also CRC_STEEP_GORGE are more interesting for us when the mech is own over these floors and not only when they are in front of the mech.
So we could change the Mech_CheckRoomCollisions() function with a new input parameter to set if we wish know the floor in front or that below the mech.
In according with this parameter it will be checked only some CRC_ in front, or others below current position.


Checking if a given input command is acceptable

When you receive a input command like UP, we should verify if from current animation of mechwarrior is acceptable this command, because the player could hit UP command in any situation, also when the mechwarrior is falling down, but our code should filter these unreasonable input commands.
More, the next animation (using method of next state id) to force, could be different in according with current animation.
For this reason, in Mech_InputCommands() function, everytime we detected an input command, we should test current animation of mechwarrior and discover what and if it is possible, a new state id to force in according with that command.
For instance, when there is ROLL command to quit driving and get off lara, we should verify not only if currently the mech is idle, but also if there is enough space in front of the mechwarrior to host lara once she jumped down.


How to handle the collision check, when the mechwarrior is going at reverse

In this situation, we'll have to check backward of mechwarrior and not forward. Anyway if we supply the current horizontal speed the value will be negative and the Mech_CheckRoomCollisions() funtion may understand the different situation checking the sign of Distance parameter, since it was the horizontal speed.
But there is another problem...
We have to check in advance if there is free space, before moving down, but when the mech is still and it has been just received the DOWN command, the Mech_CheckRoomCollisions() function could be skipped, own because mech is currently still
We have to fix this problem, trying to detect in advance if there is the decision to try to move mech down (or forward) and to call the Mech_CheckRoomCollisions() function also when the mechwarrior is still but there is currently an UP or DOWN command.
In this job, we should suppose a speed also when it is currently missing, in according with current game commands. So if the real speed is 0, but there is the UP command we'll set a theoric not null, positive speed, while when there is the DOWN command, we'll set a theoric negative speed.
Then we'll use this value of the speed, real or theoric, to decide if call the Mech_CheckRoomCollisions() function


Final code of Mech Warrior

Unfortunately the time required to build all possible skills of Mech Warrior is too long for me and for this reason it could stop my job about other plugin features and forbid the final release of plugin trng release.
For this reason I have to stop my development of Mech Warrior source.
In spite of this bad news, you can find in demo plugin source an updated release of Mech Warrior code, where it's possible get in/out and move mech in all directions with some special move.
In following table you can see the list of input command already set in Mec Warrior code


Final Input Commands for Mech Warrior

Input Commands for Mech Warrior

Command Action Description
ACTION Get-in or Special Move When lara is not on board with ACTION key she will get in (if she in correct, in-front, position)
When Lara is already on board this command works only if mech is idle (no movements)
Currently there two special moves already working:
If Mech has in front a two click obstacle, hitting ACTION + UP (just a moment first ACTION and then UP), mech will climb that obstacle with a single long step.
If Mech has backward a two clicks hole, hitting ACTION = DOWN (just a moment first ACTION and then DOWN), mech will move down in the back hole with a single long step
ROLL Get-Out Lara will get off from mech. This action will be performed only if in front of mech there will be the required free space and the floor has the same height of floor where the mech stands
UP Move Forward UP can be mixed with LEFT or RIGHT to turn running forward or with ACTION command (see ACTION description)
DOWN Move backward To use with no other movement command excepting Action (see description of ACTION command. Mech warrior will move backward
STEP LEFT Move to left side A step at left It's possible only from idle state or previous step left
STEP RIGHT Move to right side A step at right It's possible only from idle state or previous step right
LEFT Turn itself at left Used alone (no UP command) it will turn mechwarrior on itself turning at left on its axis
RIGHT Turn itself at right Used alone (no UP command) it will turn mechwarrior on itself turning at right on its axis
WALK Proteced Moving When you keep down WALK command while mech is moving it will be removed the risk to fall in the empty


Homeworks for Mech Warrior

Oh well, since in this case I had to stop my development, the homeworks are really a lot....
Anyway speaking only some suggestions I can rememer following improvments


To handle special moves for kicking and beating
If you see All Animations of Mechwarrior you find two animation not yet handle by current code:

26          2          11          Kicking animation. Mechwarrior kicks with right foot and then comes back to idle animation
27          2          12          Beating animation. Mechwarrior hits with both arms in front of it and then comes back to idle animation

They have been created by Psiko to destroy, hurting or killing objects or enemies.
A way to handle these moves is to use ACTION (or other unused command) when mech is idle and no other command has been set.
When the right command has been detected, the code should check the position in front of mech. You can read the code about DetectCollisionWithStatics() to see how detect object in some relative position, then just change the enumFIND argument with other to have also moveables.
When you detect a valid object to kick/beat you can apply some damage or movement for that object. For instance: explode for statics, killing or injuring for enemies, opening for doors ect.


Manage collision with enemies or statics
What should it happen when Mech touch some object?
Most reasonable behaviour is to destroy shatter object and killing enemies.
For killing enemies you can use pratically same RobotCheckEnemyCollisions() function, since the behaviour is the same you wish get. Furherly you could add some code to manage also shatter objects and destroy them.


What will it happen when Mech is "swimming" in the water, quick sands or lava rooms?
You can set what you wish anyway I suggest to compute, using CheckFloor() function, the height of water or the position in specific room (for lava and quicksands and you'll use always CheckFloor() function for that) and then if level of water or quicksand is enough to reach mouth of Lara you'll simulate the air leaking with dead if for too time she in that situation, while for Lava the position should be computed with feet position of lara, since just lava touches her body to kill her.
About movements you should slow down forward running when feet of Mech are in water/quicksands, this it is possible with a little trick...
If you, in code at end of Mech_NormalizeCoordinate() function, detected the need to slow down speed, just you reduce in same way speed H and/or speed V.
Pratically you should add your code in this position of Mech_NormalizeCoordinate() function

          // HERE, you reduce the SpeedH and/or SpeedV of pMech object, and then it will be copied also to Lara speed fields.
          // ------- copy speed data -------------------------------
          GET.pLara->SpeedH = pMech->SpeedH;
          GET.pLara->SpeedV = pMech->SpeedV;


Will be able Shooting enemies to injury Lara?
You should handle also this situation: Lara is on board of Mech and some enemies shot her. What will it happen?
You can set what you wish. If you wish thinking that Mech protect fully her from shooting you'lll have to get her invulnerable upon she is on board, otherwise you can let default situation where Lara will be hurt and when she dies to handle only dead animation.

Shooting skill for Mech Warrior
Psiko crafted MechWarrior with two lateral in top side gun machines. You should try to get working them to shoot and kill enemies.
Truely, it's not easy currently adding this skill because I've not yed supplied an easy way for shooting features. Anyway it may be that, for when this release will be developed, I'll have add that feature.



Reference to external Help Files

How to download and install Microsoft Visual Express 2010 program
Table A: Source File Descriptions
Table B: Sections of Main Source (Plugin_trng.cpp)
Callback and TRNG Services
Table B: Function Collection
Basics of C++ Language
How to customize Visual Express
Creation of new Triggers
Plugin Run-time Files
Debugger Tutorial
How to create dynamically Script Commands




Summary

Introduction
Who is able to create plugins?
          Who are you?
          Different levels of knowledge
Installing Microsoft Visual Express 2010
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