Initial sauer
[SauerbratenRemote.git] / src / fpsgame / monster.h
blobaf623e67423cfb8bfb1b98ecec08e1cf61cbfab9
1 // monster.h: implements AI for single player monsters, currently client only
3 struct monsterset
5 fpsclient &cl;
6 vector<extentity *> &ents;
7 vector<int> teleports;
9 static const int TOTMFREQ = 13;
10 static const int NUMMONSTERTYPES = 8;
12 struct monstertype // see docs for how these values modify behaviour
14 short gun, speed, health, freq, lag, rate, pain, loyalty, bscale, weight;
15 short painsound, diesound;
16 const char *name, *mdlname, *vwepname;
19 struct monster : fpsent
21 fpsclient &cl;
23 monstertype *monstertypes;
24 monsterset *ms;
26 int monsterstate; // one of M_*, M_NONE means human
28 int mtype; // see monstertypes table
29 fpsent *enemy; // monster wants to kill this entity
30 float targetyaw; // monster wants to look in this direction
31 int trigger; // millis at which transition to another monsterstate takes place
32 vec attacktarget; // delayed attacks
33 int anger; // how many times already hit by fellow monster
35 monster(int _type, int _yaw, int _state, int _trigger, int _move, monsterset *_ms) : cl(_ms->cl), monstertypes(_ms->monstertypes), ms(_ms), monsterstate(_state)
37 type = ENT_AI;
38 respawn();
39 if(_type>=NUMMONSTERTYPES || _type < 0)
41 conoutf("warning: unknown monster in spawn: %d", _type);
42 _type = 0;
44 monstertype *t = monstertypes+(mtype = _type);
45 eyeheight = 8.0f;
46 aboveeye = 7.0f;
47 radius *= t->bscale/10.0f;
48 xradius = yradius = radius;
49 eyeheight *= t->bscale/10.0f;
50 aboveeye *= t->bscale/10.0f;
51 weight = t->weight;
52 if(_state!=M_SLEEP) cl.spawnplayer(this);
53 trigger = cl.lastmillis+_trigger;
54 targetyaw = yaw = (float)_yaw;
55 move = _move;
56 enemy = cl.player1;
57 gunselect = t->gun;
58 maxspeed = (float)t->speed*4;
59 health = t->health;
60 armour = 0;
61 loopi(NUMGUNS) ammo[i] = 10000;
62 pitch = 0;
63 roll = 0;
64 state = CS_ALIVE;
65 anger = 0;
66 s_strcpy(name, t->name);
69 // monster AI is sequenced using transitions: they are in a particular state where
70 // they execute a particular behaviour until the trigger time is hit, and then they
71 // reevaluate their situation based on the current state, the environment etc., and
72 // transition to the next state. Transition timeframes are parametrized by difficulty
73 // level (skill), faster transitions means quicker decision making means tougher AI.
75 void transition(int _state, int _moving, int n, int r) // n = at skill 0, n/2 = at skill 10, r = added random factor
77 monsterstate = _state;
78 move = _moving;
79 n = n*130/100;
80 trigger = cl.lastmillis+n-ms->skill()*(n/16)+rnd(r+1);
83 void monsteraction(int curtime) // main AI thinking routine, called every frame for every monster
85 if(enemy->state==CS_DEAD) { enemy = cl.player1; anger = 0; }
86 normalize_yaw(targetyaw);
87 if(targetyaw>yaw) // slowly turn monster towards his target
89 yaw += curtime*0.5f;
90 if(targetyaw<yaw) yaw = targetyaw;
92 else
94 yaw -= curtime*0.5f;
95 if(targetyaw>yaw) yaw = targetyaw;
97 float dist = enemy->o.dist(o);
98 if(monsterstate!=M_SLEEP) pitch = asin((enemy->o.z - o.z) / dist) / RAD;
100 if(blocked) // special case: if we run into scenery
102 blocked = false;
103 if(!rnd(20000/monstertypes[mtype].speed)) // try to jump over obstackle (rare)
105 jumpnext = true;
107 else if(trigger<cl.lastmillis && (monsterstate!=M_HOME || !rnd(5))) // search for a way around (common)
109 targetyaw += 90+rnd(180); // patented "random walk" AI pathfinding (tm) ;)
110 transition(M_SEARCH, 1, 100, 1000);
114 float enemyyaw = -(float)atan2(enemy->o.x - o.x, enemy->o.y - o.y)/RAD+180;
116 switch(monsterstate)
118 case M_PAIN:
119 case M_ATTACKING:
120 case M_SEARCH:
121 if(trigger<cl.lastmillis) transition(M_HOME, 1, 100, 200);
122 break;
124 case M_SLEEP: // state classic sp monster start in, wait for visual contact
126 if(editmode) break;
127 normalize_yaw(enemyyaw);
128 float angle = (float)fabs(enemyyaw-yaw);
129 if(dist<32 // the better the angle to the player, the further the monster can see/hear
130 ||(dist<64 && angle<135)
131 ||(dist<128 && angle<90)
132 ||(dist<256 && angle<45)
133 || angle<10
134 || (ms->monsterhurt && o.dist(ms->monsterhurtpos)<128))
136 vec target;
137 if(raycubelos(o, enemy->o, target))
139 transition(M_HOME, 1, 500, 200);
140 playsound(S_GRUNT1+rnd(2), &o);
143 break;
146 case M_AIMING: // this state is the delay between wanting to shoot and actually firing
147 if(trigger<cl.lastmillis)
149 lastaction = 0;
150 attacking = true;
151 cl.ws.shoot(this, attacktarget);
152 transition(M_ATTACKING, 0, 600, 0);
154 break;
156 case M_HOME: // monster has visual contact, heads straight for player and may want to shoot at any time
157 targetyaw = enemyyaw;
158 if(trigger<cl.lastmillis)
160 vec target;
161 if(!raycubelos(o, enemy->o, target)) // no visual contact anymore, let monster get as close as possible then search for player
163 transition(M_HOME, 1, 800, 500);
165 else
167 bool melee = false, longrange = false;
168 switch(monstertypes[mtype].gun)
170 case GUN_BITE:
171 case GUN_FIST: melee = true; break;
172 case GUN_RIFLE: longrange = true; break;
174 // the closer the monster is the more likely he wants to shoot,
175 if((!melee || dist<20) && !rnd(longrange ? (int)dist/12+1 : min((int)dist/12+1,6)) && enemy->state==CS_ALIVE) // get ready to fire
177 attacktarget = target;
178 transition(M_AIMING, 0, monstertypes[mtype].lag, 10);
180 else // track player some more
182 transition(M_HOME, 1, monstertypes[mtype].rate, 0);
186 break;
190 if(move || moving || (onplayer && (onplayer->state!=CS_ALIVE || lastmoveattempt <= onplayer->lastmove)))
192 vec pos(o);
193 pos.sub(eyeheight);
194 loopv(ms->teleports) // equivalent of player entity touch, but only teleports are used
196 entity &e = *ms->ents[ms->teleports[i]];
197 float dist = e.o.dist(pos);
198 if(dist<16) cl.et.teleport(ms->teleports[i], this);
201 moveplayer(this, 2, false); // use physics to move monster
205 void monsterpain(int damage, fpsent *d)
207 if(d->type==ENT_AI) // a monster hit us
209 if(this!=d) // guard for RL guys shooting themselves :)
211 anger++; // don't attack straight away, first get angry
212 int _anger = d->type==ENT_AI && mtype==((monster *)d)->mtype ? anger/2 : anger;
213 if(_anger>=monstertypes[mtype].loyalty) enemy = d; // monster infight if very angry
216 else if(d->type==ENT_PLAYER) // player hit us
218 anger = 0;
219 enemy = d;
220 ms->monsterhurt = true;
221 ms->monsterhurtpos = o;
223 cl.ws.damageeffect(damage, this);
224 if((health -= damage)<=0)
226 state = CS_DEAD;
227 lastpain = cl.lastmillis;
228 playsound(monstertypes[mtype].diesound, &o);
229 ms->monsterkilled();
230 superdamage = -health;
231 cl.ws.superdamageeffect(vel, this);
233 else
235 transition(M_PAIN, 0, monstertypes[mtype].pain, 200); // in this state monster won't attack
236 playsound(monstertypes[mtype].painsound, &o);
241 monstertype *monstertypes;
243 monsterset(fpsclient &_cl) : cl(_cl), ents(_cl.et.ents)
245 static monstertype _monstertypes[NUMMONSTERTYPES] =
247 { GUN_FIREBALL, 15, 100, 3, 0, 100, 800, 1, 10, 90, S_PAINO, S_DIE1, "an ogro", "monster/ogro", "monster/ogro/vwep"},
248 { GUN_CG, 18, 70, 2, 70, 10, 400, 2, 10, 50, S_PAINR, S_DEATHR, "a rhino", "monster/rhino", NULL},
249 { GUN_SG, 13, 120, 1, 100, 300, 400, 4, 14, 115, S_PAINE, S_DEATHE, "ratamahatta", "monster/rat", "monster/rat/vwep"},
250 { GUN_RIFLE, 14, 200, 1, 80, 400, 300, 4, 18, 145, S_PAINS, S_DEATHS, "a slith", "monster/slith", "monster/slith/vwep"},
251 { GUN_RL, 12, 500, 1, 0, 200, 200, 6, 24, 210, S_PAINB, S_DEATHB, "bauul", "monster/bauul", "monster/bauul/vwep"},
252 { GUN_BITE, 22, 50, 3, 0, 100, 400, 1, 15, 75, S_PAINP, S_PIGGR2, "a hellpig", "monster/hellpig", NULL},
253 { GUN_ICEBALL, 11, 250, 1, 0, 10, 400, 6, 18, 160, S_PAINH, S_DEATHH, "a knight", "monster/knight", "monster/knight/vwep"},
254 { GUN_SLIMEBALL, 15, 100, 1, 0, 200, 400, 2, 10, 60, S_PAIND, S_DEATHD, "a goblin", "monster/goblin", "monster/goblin/vwep"},
256 monstertypes = _monstertypes;
259 void preloadmonsters()
261 loopi(NUMMONSTERTYPES) loadmodel(monstertypes[i].mdlname, -1, true);
264 vector<monster *> monsters;
266 int nextmonster, spawnremain, numkilled, monstertotal, mtimestart, remain;
268 bool monsterhurt;
269 vec monsterhurtpos;
271 IVAR(skill, 1, 3, 10);
273 void spawnmonster() // spawn a random monster according to freq distribution in DMSP
275 int n = rnd(TOTMFREQ), type;
276 for(int i = 0; ; i++) if((n -= monstertypes[i].freq)<0) { type = i; break; }
277 monsters.add(new monster(type, rnd(360), M_SEARCH, 1000, 1, this));
280 void monsterclear(int gamemode) // called after map start or when toggling edit mode to reset/spawn all monsters to initial state
282 removetrackedparticles();
283 loopv(monsters) delete monsters[i];
284 cleardynentcache();
285 monsters.setsize(0);
286 numkilled = 0;
287 monstertotal = 0;
288 spawnremain = 0;
289 remain = 0;
290 monsterhurt = false;
291 if(m_dmsp)
293 nextmonster = mtimestart = cl.lastmillis+10000;
294 monstertotal = spawnremain = gamemode<0 ? skill()*10 : 0;
296 else if(m_classicsp)
298 mtimestart = cl.lastmillis;
299 loopv(ents) if(ents[i]->type==MONSTER)
301 monster *m = new monster(ents[i]->attr2, ents[i]->attr1, M_SLEEP, 100, 0, this);
302 monsters.add(m);
303 m->o = ents[i]->o;
304 entinmap(m);
305 monstertotal++;
308 teleports.setsizenodelete(0);
309 if(m_dmsp || m_classicsp)
311 loopv(ents) if(ents[i]->type==TELEPORT) teleports.add(i);
315 void endsp(bool allkilled)
317 conoutf(allkilled ? "\f2you have cleared the map!" : "\f2you reached the exit!");
318 monstertotal = 0;
319 cl.cc.addmsg(SV_FORCEINTERMISSION, "r");
322 void monsterkilled()
324 numkilled++;
325 cl.player1->frags = numkilled;
326 remain = monstertotal-numkilled;
327 if(remain>0 && remain<=5) conoutf("\f2only %d monster(s) remaining", remain);
330 void monsterthink(int curtime, int gamemode)
332 if(m_dmsp && spawnremain && cl.lastmillis>nextmonster)
334 if(spawnremain--==monstertotal) { conoutf("\f2The invasion has begun!"); playsound(S_V_FIGHT); }
335 nextmonster = cl.lastmillis+1000;
336 spawnmonster();
339 if(monstertotal && !spawnremain && numkilled==monstertotal) endsp(true);
341 bool monsterwashurt = monsterhurt;
343 loopv(monsters)
345 if(monsters[i]->state==CS_ALIVE) monsters[i]->monsteraction(curtime);
346 else if(monsters[i]->state==CS_DEAD)
348 if(cl.lastmillis-monsters[i]->lastpain<2000)
350 //monsters[i]->move = 0;
351 monsters[i]->move = monsters[i]->strafe = 0;
352 moveplayer(monsters[i], 2, false);
357 if(monsterwashurt) monsterhurt = false;
360 void monsterrender()
362 loopv(monsters)
364 monster &m = *monsters[i];
365 if(m.state!=CS_DEAD || m.superdamage<50)
367 modelattach vwep[] = { { monstertypes[m.mtype].vwepname, MDL_ATTACH_VWEP, ANIM_VWEP|ANIM_LOOP, 0 }, { NULL } };
368 renderclient(&m, monstertypes[m.mtype].mdlname, vwep, m.monsterstate==M_ATTACKING ? -ANIM_SHOOT : 0, 300, m.lastaction, m.lastpain);