The new Avian AI has been implemented.
There are just a few remaining tasks to complete, and then the AI will be refactored, with the new orders features added.
first off, i still have to hook up the avian ai:
the current code:
you can see how avians were originally implemented as a bunch of special cases throughout the animal AI code.
you can also see where the old land animal AI has been turned off, and the new land animal AI gets called instead.
All three versions of the AI are still in the code: the original AI, the setstate/runstate refactored AI, and the new AI.
Most of the old AI code is turned off via comment blocks. function level linking takes care of the rest.
void move_animals(){int a,t;if (turns_per_render > 128) return;if (last_active_animal < 0) return;for (a=0; a <= last_active_animal; a++) { if (!animal[a].active) continue; if (!animal[a].alive) continue; t=animal[a].type; if (animaltype[t].avian) { if (animal[a].suprised) { move_suprised_avian(a); continue; } else if (animaltype[t].AI==ATTACK_AI) avian_predator_setstate(a); else setstate(a); runstate(a); } else { if (animal[a].suprised) continue; // setstate(a); // runstate(a); B2_run_animal_AI(a); } }}
the new code will look more like:
void move_animals(){int a,t;if (turns_per_render > 128) return;if (last_active_animal < 0) return;for (a=0; a <= last_active_animal; a++) { if (!animal[a].active) continue; if (!animal[a].alive) continue; t=animal[a].type; if (animaltype[t].avian) B2_run_avian_AI(a); else { if (animal[a].suprised) continue; B2_run_animal_AI(a); } }}
I also need to add fatigue modeling to the animal and avian movement code in the new AI.
Here's a snippet of code from the old AI that does fatigue modeling and sets the movement amount all at once.
This will be adapted to create a routine that increases fatigue based on the animal's current movement rate.
This "increase_fatigue" routine will then be called by the land animal and avian move routines.
if (animal[a].fatigue>=100000) { animal[a].fatigue-=7; return; }t=animal[a].type;spd=animal_speed(a);if (running) { if (animal[a].fatigue<100000) { spd*=4.0f; animal[a].fatigue+=64; if (animal[a].fatigue>100000) animal[a].fatigue=100000; } else { spd *= 2.0f; animal[a].fatigue+=4; if (animal[a].fatigue>100000) animal[a].fatigue=100000; } }else { animal[a].fatigue+=2; if (animal[a].fatigue>100000) animal[a].fatigue=100000; }
i also need to add modeling of sneak detection for hired NPC warriors and NPC travelling companions. while they are NPCs,
they are friendly with the player, and the player can give them combat orders. One of the orders they can be given is to
use sneak mode. So i need to make a version of the sneak detection code that works with animals. Cavemen are just another
animal type (species), type 2 to be specific: homo sapien. Hippidion is type 0, smilodon is type 1. pterasaur is type 3,
in anticipation of the bring on the dinos option. Yes, you can punch up a pterasaur encounter and tkae them on with a
spear and throwing rocks!
encounter tables.
The current code only works for bandmembers. its that same old design question we all face when starting a game project: should
i make the player part of the entity list or keep them separate? keeping them in the list means possible code reuse (a good thing).
but usually you need much more data about the player then other entities. that leads to designs where some data is in the entity
list, and the rest is elsewhere. somewhat more complex (a bad thing). a lot depends on whether you can get code savings.
A C-E apprach for components shared by both player and non-player entities seesm like a good idea, but can also be a bit
more complex than a brute force implementation. In my Airships simulator, i model the players ship in great detail, then copy
the pertinant results to entity #0 which is predfeined to be the player's ship. This gives me a simple player airship data
structure, but can still use generic entity code for things like movement, collisions, drawing, etc.
In Caveman, there's isnt a single PC like in most single player games. The human player can control up to 10 PCs. So Caveman
has two entity lists: one for PCs, and one for eveything else. The data for a PC is much more than that for an animal
(everything else). so they are two different types of data strutres stored in two separate lists. Right now, the sneak code work
with a cavemanrec, but not with an animalrec. note that this code is actually in CScript, a c++ macro processor/code generator
i wrote and use in-house. You can freely mix c++ and Cscript code. the first line of BMmodel_sneak_detection() reads:
function void BMmodel_sneak_detection int a
the . at the end is a close squiggly.
This code includes raypick /raycasting style line-of-sight testing.
Note that Caveman differentiates between detected by friendly and detected by hostile. That way you're not detected just becasue
your horse can see you (Oblivion!).
int detection_range(int a) // returns dist at which player a can be seen{int x,z;x=cm[a].mx;z=cm[a].mz;switch(map[x][z].coverage) { case WOODS: case GRASS: case JUNGLE: return(50); }return(300);} // returns 1 if there's a clear line-of-sight from location L to loction L2int clear_LOS(location *L,location *L2){float dx,dy,dz, // displacement & normalized direction vector from p1 to p2 x,y,z, // the point we raycast from p1 to p2 mag; // magnitude of displacement vector from p1 to p2 int mx,mz, // mx,mz of the point we raycast from p1 to p2 a; // loop counter. the number of steps iterated so far in the raycast.location L3; // location of our raycast point for call to terrain_in_way()x = L2->x + (L2->mx - L->mx) * 26400.0f; // convert x,z of p2 to p1 mx,mz relative coordsz = L2->z + (L2->mz - L->mz) * 26400.0f; dx = x - L->x; // calc displacement vector dx,dy,dz between the 2 pointsdy = L2->y - L->y;dz = z - L->z;mag=sqrt(dx*dx+dz*dz); // calc magnitude of the displacement vectormag=sqrt(mag*mag+dy*dy);dx/=mag; // divide displacment vector by mag to get normalized direction vectordy/=mag;dz/=mag;mx=L->mx; // start at 1st pointmz=L->mz;x=L->x; y=L->y;z=L->z;for (a=0; a < (int)mag; a++) // dir vec mag = 1. # steps = mag of displacement vec. { x+=dx; // move the point y+=dy; z+=dz; if (x>26400.0f) // handle map sq edges { x-=26400.0f; mx++; } if (x<0.0f) { x+=26400.0f; mx--; } if (z>26400.0f) { z-=26400.0f; mz++; } if (z<0.0f) { z+=26400.0f; mz--; } L3.mx=mx; // check for terrain collision L3.mz=mz; L3.x=x; L3.y=y; L3.z=z; if (terrain_in_way(&L3)) return(0); if (heightmap(mx,mz,x,z) >= y) return(0); // check for ground collision }return(1); // no collisions, have clear LOS, return 1.} // returns: 0=not detected. 1=detected by some animal. 2=detected by wild or hostile targeting the playerint detection_state(int a) // a is band member #{int b, // animal # dr, // detection rng of bandmember a rh, // rel heading, animal to band member rng, // range from band member to animal result, // return value targeting_player; // booleanfloat x,z;location L,L2;dr=detection_range(a);result=0;for (b=0; b.active) continue; if (! animal.alive) continue; if (animal.location != cm[cm0].location) continue; if (abs(cm[a].mx-animal.mx) > 1) continue; // more than 1 map sq away in x direction if (abs(cm[a].mz-animal.mz) > 1) continue; // more than 1 map sq away in z direction if ( ((animal.state==WILD) || (animal.state==HOSTILE)) && (animal.tgt_is_caveman==1) && (animal.tgt==a)) targeting_player=1; else targeting_player=0; if ( (result==0) || ((result==1) && (targeting_player)) ) { x=cm[a].x+(cm[a].mx-animal.mx)*26400.0f; // calc band member x,z rel to animal's map sq z=cm[a].z+(cm[a].mz-animal.mz)*26400.0f; // calc band member x,z rel to animal's map sq rng=rng2D((int)animal.x,(int)animal.z,(int)x,(int)z); if (rng > animal_cliprng-20) continue; if (rng > dr) continue; rh=relheading((int)animal.x,(int)animal.z,(int)rad2deg(animal.yr),(int)x,(int)z); // normalized, ok. if (abs(rh) > 90) continue; L.mx=animal.mx; L.mz=animal.mz; L.x=animal.x; L.y=animal.y; L.z=animal.z; L2.mx=cm[a].mx; L2.mz=cm[a].mz; L2.x=cm[a].x; L2.y=cm[a].y; L2.z=cm[a].z; if (! clear_LOS(&L,&L2) ) continue; } else continue; switch(result) { case 0: if (targeting_player) return(2); else result=1; break; case 1: return(2); } }return(result);} // returns 1 if band member a is targeted by a hostile caveman or wild animalint bandmember_targeted(int a){int b;for (b=0; b.active) continue; if (! animal.alive) continue; if ((animal.state != HOSTILE) && (animal.state != WILD)) continue; if (! animal.tgt_is_caveman) continue; if (animal.tgt != a) continue; return(1); }return(0);} fn v BMmodel_sneak_detection i aif (a % 15 != frame) return;if (cm[a].sneak_state==0) return;if (cm[a].sneak_state==3) { if (! bandmember_targeted(a)) cm[a].sneak_state=1; }else // states 1 & 2 { switch (detection_state(a)) { case 0: cm[a].sneak_state=1; // sneaking - undetected break; case 1: cm[a].sneak_state=2; // sneaking - detected - non-hostile break; case 2: cm[a].sneak_state=3; // sneaking - detected - hostile break; } }.
another thing left to do is to init the "taking fire" variables for animals when they get hit by a missile.
again, there's only code for bandmembers at the moment. More CScript. this function just sets three variables.
It gets called when a bandmember gets hit by a missile. I need to make a version for animals. IE: replace cm[] with
animal[], then call it from the code that checks missile collisions with animals.
fn v set_BM_taking_fire i cm1= cm[cm1].taking_fire 1= cm[cm1].taking_fire_counter 0= cm[cm1].taking_fire_dir (float)(dice(628)/100.0f).
another thing to do is make an avian version of do_orders, where the new AI follows the player's orders. This
is basically a copy / paste /edit of B2_do_orders to create B2_avian_do_orders. calls to B2_avian_setpitch get
added after calls to turn the animal, and avian versions of attack and move are used.
the last thing left is to add animal[a].diving to the savegame file format. Its used by avian predators to track
their attack state (diving on tgt, or climbing for next pass). the variable has already been added to the
animalrec struct declaration and is used by the code. but the vsae and load code writes individual fields, not
entire structs. so the old savegames will still load, with animal[a].diving taking a default value of zero (false)
as desired. So all i have to do is add one line of code to save the variable, then run the game to convert the
existing savegames to the new format by simply loading and saving each one (there's only one - a long term
playtest game). Then i add one line of code to read the new variable, and i'm done. file format updated, and
savegames converted. If i wanted backward compatability with savegames i didn't convert, i add an extra line of
code to save a junk value before the new data. when i read, i try to tread the junk value. if i get an EOF, its
an old format, and any variables normally read after that point will not be read and simpy keep their default
initial values. if i don't get an eof, there's more data, so i keep reading until the next "EOF test" or i've
read everything. Since changes to the file format are appended at the end of the file, you can read in something
many versions old and get the original data plus defualt values for newver variables. So the basic idea is you
init everything to default values, then read in the savegame values over them. whatever you don't read in,
uses the default values. when you save, you write everyting.