Here's the new code for bandmember AI.
In AI terms, this code is a crude expert system:
http://en.wikipedia.org/wiki/Expert_system
it is implemented primarily as a decision tree:
http://en.wikipedia.org/wiki/Decision_tree
// ############################## NEW AI: B2 API ######################### // stand is the default AI state#define B2_AISTATE_STAND 0#define B2_AISTATE_REST 1#define B2_ORDERS_NONE 0#define B2_ORDERS_FOLLOW 1#define B2_ORDERS_GOTO 2#define B2_ORDERS_ATTACK 3#define B2_ORDERS_FLEE 4#define B2_ORDERS_MAINTAIN_DIST 5 // ------------------- BAND MEMBER AI ----------------------------// level 7 void init_bandmember_CR(int a){int t, // bandmember's target rh; // rel heading to tgtif (cm[a].collision_recovery) { cm[a].collision_recovery=0; return; }cm[a].collision_recovery=1;cm[a].CR_counter=0;t=cm[a].tgt;if (t != -1) // if have tgt { rh=RH(cm[a].mx,cm[a].mz,(int)cm[a].x,(int)cm[a].z,(int)rad2deg(cm[a].yr),animal[t].mx,animal[t].mx,(int)animal[t].x,(int)animal[t].z); if (rh>0) // if tgt to right { if (dice(10000) < 7500) cm[a].CRyr=cm[a].yr+pi*0.75f; // 75% chance to turn right else cm[a].CRyr=cm[a].yr+pi*1.25f; // 25% chance to turn left } else // tgt to left { if (dice(10000) < 2500) cm[a].CRyr=cm[a].yr+pi*0.75f; // 25% chance to turn right else cm[a].CRyr=cm[a].yr+pi*1.25f; // 75% chance to turn left } }else // no tgt { if (dice(10000) < 5000) cm[a].CRyr=cm[a].yr+pi*0.75f; // 50-50 chance of left or right else cm[a].CRyr=cm[a].yr+pi*1.25f; }while (cm[a].CRyr >= pi*2.0f) cm[a].CRyr -= pi*2.0f; // nomralize to 0 to 2pi} void B2_BM_turn(int a,float amount){// a is band member #// amount is amount to turn (in rads). postive = turn right, negative = turn left.cm[a].yr+=amount;while (cm[a].yr<0.0f) { cm[a].yr+=pi*2.0f; }while (cm[a].yr>=pi*2.0f) { cm[a].yr-=pi*2.0f; }} // ------------------------------------------------------ // level 6 float B2_BM_calc_amount2move(int a){// a is BM #// w is curwpn// t is tgt typeint w,desired_rng,t;float maxspd,amount;// move 0.0 if no tgtif (cm[a].tgt == -1) { return(0.0f); }w=cm[a].curwpn;if (object[w].ishandwpn) { t=animal[cm[a].tgt].type; desired_rng=animaltype[2].rad+animaltype[t].rad; }else { desired_rng=50; }amount=(float)(cm[a].rng-desired_rng);maxspd=movement_rate(a);// multiply movement rate by two for runningmaxspd*=2.0f;if (amount < -maxspd) { amount=-maxspd; }if (amount > maxspd) { amount=maxspd; }return(amount);} void B2_BM_move(int a,float amount){// a is BM #float x,y,z,spd,vx,vz;int mx,mz, // moved,tgt,t,rh, // move_type, // 0=move atk. 1= move follow 2 = dont move old_mx,old_mz; // , // b; // who to followlocation L;if (cm[a].falling) { return; }mx=cm[a].mx;mz=cm[a].mz;x=cm[a].x;y=cm[a].y;z=cm[a].z;old_mx=cm[a].mx;old_mz=cm[a].mz;spd=amount; // movement_rate(a)*2.0f;vx=sin(cm[a].yr)*spd;vz=cos(cm[a].yr)*spd;x+=vx;z+=vz;normalize_location(&mx,&mz,&x,&z);y=heightmap(mx,mz,x,z);L.mx=mx;L.mz=mz;L.x=x;L.y=y;L.z=z;if ( (cm[a].location==OUTSIDE) && (! inpermshel(a)) && (! in_rockshelter(a)) && (terrain_in_way(&L)) ) { init_bandmember_CR(a); return; }else if ((cm[a].location==INCAVERN) && (cavern_wall_in_way(&L))) { init_bandmember_CR(a); cm_standani(a); return; }if (bandmember_in_way_of_bandmember(a,&L)) { init_bandmember_CR(a); cm_standani(a); return; }if (animal_in_way_of_bandmember(mx,mz,x,z)) { init_bandmember_CR(a); cm_standani(a); return; }if (bandmember_steep_rise_ahead(a)) // steep rise { init_bandmember_CR(a); cm_standani(a); return; }if (steep_drop(cm[a].y,y,spd)) // steep drop { cm[a].falling=1; cm[a].vy=0.0f; y=cm[a].y; }cm[a].x=x;cm[a].y=y;cm[a].z=z;cm[a].mx=mx;cm[a].mz=mz;cm[a].vx=vx;cm[a].vz=vz;automap(a,old_mx,old_mz,mx,mz,(int)x,(int)z);increase_fatigue(a);if (cm[a].camoflaged) { if ( cm[a].camotype != map[cm[a].mx][cm[a].mz].coverage ) { msg("The terrian has changed and you are no longer camoflaged!"); cm[a].camoflaged=0; } }cm[a].moved=1;} // returns rel heading from yr to desired_dir. all units are in rads.float B2_calc_rh(float yr,float desired_dir){float rh;rh=desired_dir-yr;while (rh>pi) { rh-=pi*2.0f; }while (rh<=-pi) { rh+=pi*2.0f; }return(rh);} // turn yr given desired_dir and maxturnrate. all units are in rads.void B2_turn_unit(float *yr,float desired_dir,float maxturnrate){float rh;rh=B2_calc_rh(*yr,desired_dir);if (rh > maxturnrate) { rh=maxturnrate; }if (rh < -maxturnrate) { rh=-maxturnrate; }*yr+=rh;while (*yr<0.0f) { *yr+=pi*2.0f; }while (*yr>=pi*2.0f) { *yr-=pi*2.0f; }} // returns heading from cm[a] to location L (in rads).float B2_BM_calc_heading2loc(int a,location *L){// a is bandmember// x,z is normalized x,z of tgt// h is heading in degrees// desired_dir is heading in radsfloat x,z,desired_dir;int h;normalize_coords(cm[a].mx,cm[a].mz,L->mx,L->mz,L->x,L->z,&x,&z);h=heading((int)cm[a].x,(int)cm[a].z,(int)x,(int)z);desired_dir=deg2rad((float)h);return(desired_dir);} void B2_BM_turn2loc(int a,location *L){// a is bandmemberfloat desired_dir;desired_dir=B2_BM_calc_heading2loc(a,L);B2_turn_unit(&cm[a].yr,desired_dir,animaltype[2].turnrate);} // -------------------------------------------------------- // level 5 // turns band member towards their tgt. tgt is an animal.// a is bandmember #. t is bandmembers tgt.void B2_BM_turn2tgt(int a){int t;location L;t=cm[a].tgt;L.mx=animal[t].mx;L.mz=animal[t].mz;L.x=animal[t].x;L.z=animal[t].z;B2_BM_turn2loc(a,&L);} int B2_BM_is_facing_tgt(int a){// a is BM #// t is tgt// x z is normalized location of tgt (tgt loc rel 2 cm[a] map sq )// rh is rel heading to tgtint t,rh;float x,z;t=cm[a].tgt;normalize_coords(cm[a].mx,cm[a].mz,animal[t].mx,animal[t].mz,animal[t].x,animal[t].z,&x,&z);rh=relheading( (int)cm[a].x, (int)cm[a].z, (int)rad2deg(cm[a].yr), (int)x, (int)z ); // normalized, ok.if (rh < -30) { return(0); }if (rh > 30) { return(0); }return(1);} void B2_BM_run(int a){// a is BM #float amount;amount=B2_BM_calc_amount2move(a);B2_BM_move(a,amount);if (cm[a].attacking) { B2_set_BM_atk_ani(a); }else { B2_set_BM_run_ani(a); }} void B2_BM_run_maxspd(int a){// a is BM #float amount;amount=movement_rate(a);// multiply movement rate by two for runningamount*=2.0f;B2_BM_move(a,amount);if (cm[a].attacking) { B2_set_BM_atk_ani(a); }else { B2_set_BM_run_ani(a); }} // 5 frames from start of attack to attack resolution// impact point is 4 ft from bandmember's head, in the direction of view.// dist travelled = (player run speed + animal run speed) * 5 frames // start attack range = animal rad + impact rng + dist travelledvoid do_bandmember_atk(int a){int tx,tz, // tgt x,z tgt, // bandmember's target rh, // rel heading to tgt t, // animaltype of target aniID;float start_attack_range, // range at which to begin attack tgtspd; // target's speedif (cm[a].attacking) return; tgt=cm[a].tgt;if (tgt == -1) return;t=animal[tgt].type;tgtspd=animaltype[t].speed;if (! animaltype[t].avian) tgtspd*=2.0f;start_attack_range=(movement_rate(a)*2.0f+tgtspd)*5.0f+4.0f+(float)animaltype[t].rad;if (cm[a].rng > start_attack_range) return;tx=(int)animal[tgt].x;tz=(int)animal[tgt].z;rh=RH(cm[a].mx,cm[a].mz,(int)cm[a].x,(int)cm[a].z,(int)rad2deg(cm[a].yr),animal[tgt].mx,animal[tgt].mz,tx,tz);if (abs(rh) > 45) return;cm[a].attacking=1;cm[a].attack_counter=0;aniID=get_attack_ani(cm[a].curwpn,cm[a].sex);setBMani(a,cm[a].modelID,aniID);} void set_bandmember_tgt(int a) // a is bandmember # // sets (animal) tgt. -1 if no tgt.{int b,rng,bestdist,besttgt,t;bestdist=100000;besttgt=-1;for (b=0; b.active) continue; if (! animal.alive) continue; switch (animal.state) { case SUBDUED: case CAPTURED: case TAMED: case FRIENDLY: case WARRIOR: case COMPANION: continue; } // skip if more than 1 map sq away if (animal.mx > cm[a].mx+1) continue; if (animal.mx < cm[a].mx-1) continue; if (animal.mz > cm[a].mz+1) continue; if (animal.mz < cm[a].mz-1) continue; rng=(int)dist3d2(cm[a].mx,cm[a].mz,cm[a].x,cm[a].y,cm[a].z,animal.mx,animal.mz,animal.x,animal.y,animal.z); if (rng > 200) continue; // dont target anything over 200 away t=animal.type; if ( (animal.dmg>0) && (animal.dmg >= animaltype[t].hp/2) && (animaltype[t].speed >= movement_rate(a)) ) continue; // dont target wounded fleeing animals you cant catch if ( (t != 2) && // if not caveman, (animaltype[t].AI==MAINTAIN_DISTANCE_AI) && // and AI maintains distance, (animaltype[t].speed >= movement_rate(a)) // and if they're too fast too catch... ) continue; // skip them. dont target them. if (rng=pi*2.0f) { desired_dir-=pi*2.0f; }B2_turn_unit(&cm[a].yr,desired_dir,animaltype[2].turnrate);} int badguys_nearby(int a) // a is band member #{int b,t;for (b=0; b.type; if (!animal.active) continue; if (!animal.alive) continue; if (animal.mx != cm[a].mx) continue; if (animal.mz != cm[a].mz) continue; if ( (t==2) && (animal.state==FRIENDLY) ) continue; if ( (t==2) && (animal.state==COMPANION) ) continue; if ( (t==2) && (animal.state==WARRIOR) ) continue; if (animal.state==TAMED) continue; if (animal.state==SUBDUED) continue; if (animal.state==CAPTURED) continue; if (animaltype[t].AI!=ATTACK_AI) continue; if ( dist ( (int)animal.x, (int)animal.z, (int)cm[a].x, (int)cm[a].z ) > 50 ) continue; return(1); }return(0);} // turns bandmember a towards bandmember cvoid B2_BM_turn2BM(int a,int c){location L;L.mx=cm[c].mx;L.mz=cm[c].mz;L.x=cm[c].x;L.z=cm[c].z;B2_BM_turn2loc(a,&L);} // -------------------------------------------------------------- // level 3 int B2_BM_cornered(int cm1){location L;float r;if (cm[cm1].location == INSHELTER) { return(0); }r=4.0f;L.mx=cm[cm1].mx;L.mz=cm[cm1].mz;L.x=cm[cm1].x+r;L.y=cm[cm1].y;L.z=cm[cm1].z;if (!something_in_way_of_location(&L)) { return(0); }L.x=cm[cm1].x-r;if (!something_in_way_of_location(&L)) { return(0); }L.x=cm[cm1].x;L.z=cm[cm1].z+r;if (!something_in_way_of_location(&L)) { return(0); }L.z=cm[cm1].z-r;if (!something_in_way_of_location(&L)) { return(0); }return(1);} void B2_BM_collision_recovery(int a){// a is bandmember// rh is rel heading to CRyr// amount is the amount to movefloat rh,amount;cm[a].CR_counter++;if (cm[a].CR_counter > 20) { cm[a].collision_recovery=0; }B2_turn_unit(&cm[a].yr,cm[a].CRyr,animaltype[2].turnrate);rh=B2_calc_rh(cm[a].yr,cm[a].CRyr);if ((rh>-pi/4.0f)&&(rh= BMhp(cm1)) { return(1); }return(0);} int B2_BM_tgt_is_stronger(int cm1){// returns 1 if tgts hp left is > 2x BMs hp leftint tgt,species,species_hp,tgt_hp,BM_hp;tgt=cm[cm1].tgt;species=animal[tgt].type;species_hp=animaltype[species].hp;tgt_hp=species_hp-animal[tgt].dmg;BM_hp=BMhp(cm1)-cm[cm1].dmg;BM_hp*=2;if (tgt_hp > BM_hp) { return(1); }return(0);} void B2_BM_move2player(int cm1){float amount;B2_BM_turn2BM(cm1,cm0);amount=movement_rate(cm1);amount*=2.0f;B2_BM_move(cm1,amount);} void doactionmodeB(int cm1){int a;a=7+cm[cm1].mood/13;if (frame<=a) action[cm[cm1].current_action].proceedure();} void B2_BM_follow(int cm1){// ordersdata[0] = tgt// ordersdata[1] = tgt_is_caveman// calc heading to tgt,// turn to tgt// if rng > 20, runfloat amount;int rng;if (cm[cm1].ordersdata[1] == 1) { B2_BM_turn2BM(cm1,cm[cm1].ordersdata[0]); rng=bm2bm_dist(cm1,cm[cm1].ordersdata[0]); }else { cm[cm1].tgt=cm[cm1].ordersdata[0]; B2_BM_turn2tgt(cm1); rng=a2bm_dist(cm[cm1].ordersdata[0],cm1); } if (rng > 20) { amount=movement_rate(cm1); amount*=2.0f; B2_BM_move(cm1,amount); }} void B2_BM_goto(int cm1){// ordersdata[0] = x// ordersdata[1] = z// calc heading to tgt// turn to tgt// if rng > 0, runfloat amount;int rng;location L;L.mx=cm[cm1].mx;L.mz=cm[cm1].mz;L.x=(float)cm[cm1].ordersdata[0];L.z=(float)cm[cm1].ordersdata[1];B2_BM_turn2loc(cm1,&L);amount=movement_rate(cm1);amount*=2.0f;rng=BBdist((int)cm[cm1].x,(int)cm[cm1].z,cm[cm1].ordersdata[0],cm[cm1].ordersdata[1]);if (rng > amount) { B2_BM_move(cm1,amount); }} void B2_BM_maintain_dist(int cm1){// if badgys_nearby, flee// else if have tgt, atk// else standif (badguys_nearby(cm1)) { B2_BM_flee(cm1); return; }if (cm[cm1].tgt != -1) { B2_BM_attack(cm1); }} void B2_BM_taking_fire_flee(int a){// a is band member #// rh is relative heading// amount is amount to movefloat amount,rh;B2_turn_unit(&cm[a].yr,cm[a].taking_fire_dir,animaltype[2].turnrate);rh=B2_calc_rh(cm[a].yr,cm[a].taking_fire_dir);// move BMif (((rh>-pi/4.0f)&&(rh 99%if (cm[cm1].fatigue > 99000) { cm[cm1].AIstate=B2_AISTATE_REST;// do nothing. fatigue is replenished elsewhere in update_bandmember. cm_standani(cm1); trace[2]=1; return(1); }// if restingif (cm[cm1].AIstate == B2_AISTATE_REST) {// if fatigue > 98% if (cm[cm1].fatigue > 98000) {// do nothing. fatigue is replenished elsewhere in update_bandmember. cm_standani(cm1); trace[2]=2; return(1); }// else stop resting else { cm[cm1].AIstate=B2_AISTATE_STAND; } }set_bandmember_tgt(cm1);if (B2_BM_cornered(cm1)) { B2_BM_attack(cm1); trace[2]=3; return(1); }if (cm[cm1].collision_recovery) { B2_BM_collision_recovery(cm1); trace[2]=4; return(1); }if (cm[cm1].taking_fire) { if (cm[cm1].tgt != -1) { B2_BM_attack(cm1); cm[cm1].taking_fire=0; trace[2]=5; return(1); } else { B2_BM_taking_fire_flee(cm1); cm[cm1].taking_fire_counter++; if (cm[cm1].taking_fire_counter >= 150) { cm[cm1].taking_fire=0; } trace[2]=6; return(1); } }if (B2_BM_halfdead(cm1)) { B2_BM_flee(cm1); trace[2]=7; return(1); }if (cm[cm1].tgt != -1) { if (cm[cm1].rng <= 20) { if (B2_BM_tgt_is_stronger(cm1)) { B2_BM_flee(cm1); trace[2]=8; return(1); } else { B2_BM_attack(cm1); trace[2]=9; return(1); } } }trace[2]=10;return(0);} void B2doaction(int cm1){int save;save=cm0;cm0=cm1;doactionmodeB(cm1);cm0=save;} void B2_BM_do_orders(int cm1){switch (cm[cm1].orders) { case B2_ORDERS_FOLLOW: B2_BM_follow(cm1); break; case B2_ORDERS_GOTO: B2_BM_goto(cm1); break; case B2_ORDERS_ATTACK: B2_BM_attack(cm1); break; case B2_ORDERS_FLEE: B2_BM_flee(cm1); break; case B2_ORDERS_MAINTAIN_DIST: B2_BM_maintain_dist(cm1); break; }} void B2_BM_leave_shelter(int cm1){if (cm[cm1].current_action != LEAVESHELTER) { setaction(cm1,LEAVESHELTER); }B2doaction(cm1);} // -------------------------------------------------------------------------- // LEVEL 1 (top level) 13 types of AI // 1 type of AI void B2_BM_run_AI(int cm1){int badguys;strcpy_s(tracestr[0],100,"leave shelter ticks");strcpy_s(tracestr[1],100,"do common ai ticks");strcpy_s(tracestr[2],100,"do common ai result");strcpy_s(tracestr[3],100,"bandmember");strcpy_s(tracestr[4],100,"everything else ticks");if (cm1 == cm0) { return; }Zstarttimer(1);badguys=badguys_nearby(cm1);if (badguys) { if (cm[cm1].location == INSHELTER) { B2_BM_leave_shelter(cm1); return; } }trace[0]=Zelapsedticks(1);Zstarttimer(1);if (B2_BM_do_common_AI(cm1)) { trace[1]=Zelapsedticks(1); return; }trace[1]=Zelapsedticks(1);Zstarttimer(1);if (badguys) { if (cm[cm1].current_action != DONOTHING) { if (cm[cm1].current_action == MOVE) { stop_moving(cm1); } setaction(cm1,DONOTHING); } if (cm[cm1].orders != B2_ORDERS_NONE) { B2_BM_do_orders(cm1); return; } if (B2_BM_tgt_is_stronger(cm1)) { B2_BM_flee(cm1); return; } B2_BM_attack(cm1); return; }if (cm[cm1].current_action != DONOTHING) { B2doaction(cm1); return; }if (cm[cm1].orders != B2_ORDERS_NONE) { if (cm[cm1].location == INSHELTER) { B2_BM_leave_shelter(cm1); return; } B2_BM_do_orders(cm1); return; }// standcm_standani(cm1);trace[4]=Zelapsedticks(1);} // ############################## END NEW AI: B2 API #########################
Well, I had about 10 notes of interest about this code, but the editor clipped them! And i need to get back to work...
lets see... note that some orders are like AI states. B2_BM_run_AI calls badguys_nearby just once as an optimization. set_bandmember_target is only called if and when needed as another optimization. both iterate the entities list. a further optimization would be to iterate the entites list just once, get the index and range of the closest badguy, and then just use that info for badguys_nearby and set_bandmember_target. But for the moment it seems to run fast enough.