void monsterthink(void) { if (m_dmsp && spawnremain && lastmillis()>nextmonster) { if (spawnremain--==monstertotal) con::out("The invasion has begun!"); nextmonster = lastmillis()+1000; spawnmonster(); } if (monstertotal && !spawnremain && numkilled==monstertotal) endsp(true); // equivalent of player entity touch, but only teleports are used loopv(ents) { entity &e = ents[i]; if (e.type!=TELEPORT) continue; vec3f v(float(e.x), float(e.y), 0.f); loopv(monsters) if (monsters[i]->state==CS_DEAD) { if (lastmillis()-monsters[i]->lastaction<2000) { monsters[i]->move = 0; physics::moveplayer(monsters[i], 1, false); } } else { v.z += monsters[i]->eyeheight; const float dist = distance(monsters[i]->o, v); v.z -= monsters[i]->eyeheight; if (dist<4) game::teleport((int)(&e-&ents[0]), monsters[i]); } } loopv(monsters) if (monsters[i]->state==CS_ALIVE) monsteraction(monsters[i]); }
void arenarespawn() { if (arenarespawnwait) { if (arenarespawnwait<lastmillis()) { arenarespawnwait = 0; con::out("new round starting... fight!"); respawnself(); } } else if (arenadetectwait==0 || arenadetectwait<lastmillis()) { arenadetectwait = 0; int alive = 0, dead = 0; char *lastteam = NULL; bool oneteam = true; loopv(players) if (players[i]) arenacount(players[i], alive, dead, lastteam, oneteam); arenacount(player1, alive, dead, lastteam, oneteam); if (dead>0 && (alive<=1 || (m_teammode && oneteam))) { con::out("arena round is over! next round in 5 seconds..."); if (alive) con::out("team %s is last man standing", lastteam); else con::out("everyone died!"); arenarespawnwait = int(lastmillis())+5000; arenadetectwait = int(lastmillis()) + 10000; player1->ypr.z = 0.f ; } }
void pickup(int n, game::dynent *d) { int np = 1; loopv(players) if (players[i]) np++; np = np<3 ? 4 : (np>4 ? 2 : 3); // spawn times are dependent on number of players int ammo = np*2; switch (ents[n].type) { case I_SHELLS: additem(n, d->ammo[1], ammo); break; case I_BULLETS: additem(n, d->ammo[2], ammo); break; case I_ROCKETS: additem(n, d->ammo[3], ammo); break; case I_ROUNDS: additem(n, d->ammo[4], ammo); break; case I_HEALTH: additem(n, d->health, np*5); break; case I_BOOST: additem(n, d->health, 60); break; case I_GREENARMOUR: // (100h/100g only absorbs 166 damage) if (d->armourtype==A_YELLOW && d->armour>66) break; additem(n, d->armour, 20); break; case I_YELLOWARMOUR: additem(n, d->armour, 20); break; case I_QUAD: additem(n, d->quadmillis, 60); break; case CARROT: ents[n].spawned = false; triggertime = int(lastmillis()); // TODO world::trigger(ents[n].attr1, ents[n].attr2, false); // needs to go over server for client::multiplayer break; case TELEPORT: { static int lastteleport = 0; if (lastmillis()-lastteleport<500) break; lastteleport = int(lastmillis()); teleport(n, d); } break; case JUMPPAD: { static int lastjumppad = 0; if (lastmillis()-lastjumppad<300) break; lastjumppad = int(lastmillis()); vec3f v((int)(char)ents[n].attr3/10.0f, (int)(char)ents[n].attr2/10.0f, ents[n].attr1/10.0f); player1->vel.z = 0; player1->vel += v; sound::playc(sound::JUMPPAD); } break; } }
// called after map start of when toggling edit mode to reset/spawn all monsters // to initial state void monsterclear(void) { cleanmonsters(); if (m_dmsp) { nextmonster = mtimestart = int(lastmillis())+1000; monstertotal = spawnremain = 1; // XXX mode()<0 ? skill*10 : 0; } else if (m_classicsp) { mtimestart = lastmillis(); loopv(ents) if (ents[i].type==MONSTER) { auto m = basicmonster(ents[i].attr2, ents[i].attr1, M_SLEEP, 100, 0); m->o.x = ents[i].x; m->o.y = ents[i].y; m->o.z = ents[i].z; entinmap(m); monstertotal++; } } }
dynent *newdynent() { dynent *d = (dynent*) MALLOC(sizeof(dynent)); d->o = zero; d->ypr = vec3f(270.f,0.f,0.f); d->maxspeed = 22.f; d->outsidemap = false; d->inwater = false; d->radius = 0.5f; d->o.y = d->eyeheight = 1.8f; d->aboveeye = 0.2f; d->frags = 0; d->plag = 0; d->ping = 0; d->lastupdate = int(lastmillis()); d->enemy = NULL; d->monsterstate = 0; d->name[0] = d->team[0] = 0; d->blocked = false; d->lifesequence = 0; d->state = CS_ALIVE; spawnstate(d); return d; }
void monsterpain(dynent *m, int damage, dynent *d) { if (d->monsterstate) { // a monster hit us if (m!=d) { // guard for RL guys shooting themselves :) m->anger++; // don't attack straight away, first get angry int anger = m->mtype==d->mtype ? m->anger/2 : m->anger; if (anger>=monstertypes[m->mtype].loyalty) m->enemy = d; // monster infight if very angry } } else { // player hit us m->anger = 0; m->enemy = d; } transition(m, M_PAIN, 0, monstertypes[m->mtype].pain,200); // in this state monster won't attack if ((m->health -= damage)<=0) { m->state = CS_DEAD; m->lastaction = lastmillis(); player1->frags = ++numkilled; sound::play(monstertypes[m->mtype].diesound, &m->o); int remain = monstertotal-numkilled; if (remain>0 && remain<=5) con::out("only %d monster(s) remaining", remain); } else sound::play(monstertypes[m->mtype].painsound, &m->o); }
void endsp(bool allkilled) { con::out(allkilled ? "you have cleared the map!" : "you reached the exit!"); con::out("score: %d kills in %d seconds", numkilled, (lastmillis()-mtimestart)/1000); monstertotal = 0; server::startintermission(); }
// main AI thinking routine, called every frame for every monster void monsteraction(dynent *m) { if (m->enemy->state==CS_DEAD) { m->enemy = player1; m->anger = 0; } normalise(m, m->targetyaw); if (m->targetyaw>m->ypr.x) { // slowly turn monster towards his target m->ypr.x += curtime()*0.5f; if (m->targetyaw<m->ypr.x) m->ypr.x = m->targetyaw; } else { m->ypr.x -= curtime()*0.5f; if (m->targetyaw>m->ypr.x) m->ypr.x = m->targetyaw; } const float disttoenemy = distance(m->o, m->enemy->o); m->ypr.y = atan2(m->enemy->o.y-m->o.y, disttoenemy)*180.f/float(pi); // special case: if we run into scenery if (m->blocked) { m->blocked = false; // try to jump over obstackle (rare) if (!rnd(20000/monstertypes[m->mtype].speed)) m->jumpnext = true; // search for a way around (common) else if (m->trigger<lastmillis() && (m->monsterstate!=M_HOME || !rnd(5))) { m->targetyaw += 180.f+rnd(180); // patented "random walk" AI pathfinding (tm) ;) transition(m, M_SEARCH, 1, 400, 1000); } } const auto enemyyaw = -atan2(m->enemy->o.x-m->o.x, m->enemy->o.z-m->o.z)/float(pi)*180.f+180.f; switch (m->monsterstate) { case M_PAIN: case M_ATTACKING: case M_SEARCH: if (m->trigger<lastmillis()) transition(m, M_HOME, 1, 100, 200); break; // state classic sp monster start in, wait for visual contact case M_SLEEP: { vec3f target; // skip running physics if (edit::mode() || !enemylos(m, target)) return; normalise(m, enemyyaw); const auto angle = abs(enemyyaw-m->ypr.x); if (disttoenemy<8.f || // the better the angle to the player (disttoenemy<16.f && angle<135.f) || // the further the monster can (disttoenemy<32.f && angle<90.f) || // see/hear (disttoenemy<64.f && angle<45.f) || angle<10) { transition(m, M_HOME, 1, 500, 200); sound::play(sound::GRUNT1+rnd(2), &m->o); } } break; // this state is the delay between wanting to shoot and actually firing case M_AIMING: if (m->trigger<lastmillis()) { m->lastaction = 0; m->attacking = true; shoot(m, m->attacktarget); transition(m, M_ATTACKING, 0, 600, 0); } break; // monster has visual contact, heads straight for player and may want to // shoot at any time case M_HOME: m->targetyaw = enemyyaw; if (m->trigger<lastmillis()) { vec3f target; // no visual contact anymore, let monster get as close as possible then // search for player if (!enemylos(m, target)) transition(m, M_HOME, 1, 800, 500); else { // the closer the monster is the more likely he wants to shoot if (!rnd((int)disttoenemy/3+1) && m->enemy->state==CS_ALIVE) { // get ready to fire m->attacktarget = target; transition(m, M_AIMING, 0, monstertypes[m->mtype].lag, 10); } else // track player some more transition(m, M_HOME, 1, monstertypes[m->mtype].rate, 0); } } break; } physics::moveplayer(m, 1, false); // use physics to move monster }
// monster AI is sequenced using transitions: they are in a particular state // where they execute a particular behaviour until the trigger time is hit, and // then they reevaluate their situation based on the current state, the // environment etc., and transition to the next state. Transition timeframes are // parametrized by difficulty level (skill), faster transitions means quicker // decision making means tougher AI. // n = at skill 0, n/2 = at skill 10, r = added random factor void transition(dynent *m, int state, int moving, int n, int r) { m->monsterstate = state; m->move = moving; n = n*130/100; m->trigger = lastmillis()+n-skill*(n/16)+rnd(r+1); }