1 // monster.h: implements AI for single player monsters, currently client only
6 vector
<extentity
*> &ents
;
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
23 monstertype
*monstertypes
;
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
)
39 if(_type
>=NUMMONSTERTYPES
|| _type
< 0)
41 conoutf("warning: unknown monster in spawn: %d", _type
);
44 monstertype
*t
= monstertypes
+(mtype
= _type
);
47 radius
*= t
->bscale
/10.0f
;
48 xradius
= yradius
= radius
;
49 eyeheight
*= t
->bscale
/10.0f
;
50 aboveeye
*= t
->bscale
/10.0f
;
52 if(_state
!=M_SLEEP
) cl
.spawnplayer(this);
53 trigger
= cl
.lastmillis
+_trigger
;
54 targetyaw
= yaw
= (float)_yaw
;
58 maxspeed
= (float)t
->speed
*4;
61 loopi(NUMGUNS
) ammo
[i
] = 10000;
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
;
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
90 if(targetyaw
<yaw
) yaw
= targetyaw
;
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
103 if(!rnd(20000/monstertypes
[mtype
].speed
)) // try to jump over obstackle (rare)
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;
121 if(trigger
<cl
.lastmillis
) transition(M_HOME
, 1, 100, 200);
124 case M_SLEEP
: // state classic sp monster start in, wait for visual contact
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)
134 || (ms
->monsterhurt
&& o
.dist(ms
->monsterhurtpos
)<128))
137 if(raycubelos(o
, enemy
->o
, target
))
139 transition(M_HOME
, 1, 500, 200);
140 playsound(S_GRUNT1
+rnd(2), &o
);
146 case M_AIMING
: // this state is the delay between wanting to shoot and actually firing
147 if(trigger
<cl
.lastmillis
)
151 cl
.ws
.shoot(this, attacktarget
);
152 transition(M_ATTACKING
, 0, 600, 0);
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
)
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);
167 bool melee
= false, longrange
= false;
168 switch(monstertypes
[mtype
].gun
)
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);
190 if(move
|| moving
|| (onplayer
&& (onplayer
->state
!=CS_ALIVE
|| lastmoveattempt
<= onplayer
->lastmove
)))
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
220 ms
->monsterhurt
= true;
221 ms
->monsterhurtpos
= o
;
223 cl
.ws
.damageeffect(damage
, this);
224 if((health
-= damage
)<=0)
227 lastpain
= cl
.lastmillis
;
228 playsound(monstertypes
[mtype
].diesound
, &o
);
230 superdamage
= -health
;
231 cl
.ws
.superdamageeffect(vel
, this);
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
;
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
];
293 nextmonster
= mtimestart
= cl
.lastmillis
+10000;
294 monstertotal
= spawnremain
= gamemode
<0 ? skill()*10 : 0;
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);
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!");
319 cl
.cc
.addmsg(SV_FORCEINTERMISSION
, "r");
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;
339 if(monstertotal
&& !spawnremain
&& numkilled
==monstertotal
) endsp(true);
341 bool monsterwashurt
= monsterhurt
;
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;
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
);