Artifact df6f0d84fe4564fc134839852d241813b6ade92bc3736f271945aae05965951a:
- File
src/Monster.m
— part of check-in
[304230c1e1]
at
2025-03-23 21:52:54
on branch trunk
— Avoid pointless pointers
Passing OFVector3D by reference is annoying and is worse at passing via registers. (user: js, size: 13359) [annotate] [blame] [check-ins using]
// monster.cpp: implements AI for single player monsters, currently client only #import "Monster.h" #include "cube.h" #import "DynamicEntity.h" #import "Entity.h" static OFMutableArray<Monster *> *monsters; static int nextmonster, spawnremain, numkilled, monstertotal, mtimestart; @implementation Monster + (void)initialize { monsters = [[OFMutableArray alloc] init]; } + (OFMutableArray<Monster *> *)monsters { return monsters; } + (instancetype)monsterWithType:(int)type yaw:(int)yaw state:(int)state trigger:(int)trigger move:(int)move { return [[self alloc] initWithType:type yaw:yaw state:state trigger:trigger move:move]; } VARF(skill, 1, 3, 10, conoutf(@"skill is now %d", skill)); // for savegames + (void)restoreAll { for (Monster *monster in monsters) if (monster.state == CS_DEAD) numkilled++; } #define TOTMFREQ 13 #define NUMMONSTERTYPES 8 struct monstertype // see docs for how these values modify behaviour { short gun, speed, health, freq, lag, rate, pain, loyalty, mscale, bscale; short painsound, diesound; OFConstantString *name, *mdlname; } monstertypes[NUMMONSTERTYPES] = { { GUN_FIREBALL, 15, 100, 3, 0, 100, 800, 1, 10, 10, S_PAINO, S_DIE1, @"an ogre", @"monster/ogro" }, { GUN_CG, 18, 70, 2, 70, 10, 400, 2, 8, 9, S_PAINR, S_DEATHR, @"a rhino", @"monster/rhino" }, { GUN_SG, 14, 120, 1, 100, 300, 400, 4, 14, 14, S_PAINE, S_DEATHE, @"ratamahatta", @"monster/rat" }, { GUN_RIFLE, 15, 200, 1, 80, 300, 300, 4, 18, 18, S_PAINS, S_DEATHS, @"a slith", @"monster/slith" }, { GUN_RL, 13, 500, 1, 0, 100, 200, 6, 24, 24, S_PAINB, S_DEATHB, @"bauul", @"monster/bauul" }, { GUN_BITE, 22, 50, 3, 0, 100, 400, 1, 12, 15, S_PAINP, S_PIGGR2, @"a hellpig", @"monster/hellpig" }, { GUN_ICEBALL, 12, 250, 1, 0, 10, 400, 6, 18, 18, S_PAINH, S_DEATHH, @"a knight", @"monster/knight" }, { GUN_SLIMEBALL, 15, 100, 1, 0, 200, 400, 2, 13, 10, S_PAIND, S_DEATHD, @"a goblin", @"monster/goblin" }, }; - (instancetype)initWithType:(int)type yaw:(int)yaw state:(int)state trigger:(int)trigger move:(int)move { self = [super init]; if (type >= NUMMONSTERTYPES) { conoutf(@"warning: unknown monster in spawn: %d", type); type = 0; } struct monstertype *t = &monstertypes[(self.monsterType = type)]; self.eyeHeight = 2.0f; self.aboveEye = 1.9f; self.radius *= t->bscale / 10.0f; self.eyeHeight *= t->bscale / 10.0f; self.aboveEye *= t->bscale / 10.0f; self.monsterState = state; if (state != M_SLEEP) spawnplayer(self); self.trigger = lastmillis + trigger; self.targetYaw = self.yaw = (float)yaw; self.move = move; self.enemy = player1; self.gunSelect = t->gun; self.maxSpeed = (float)t->speed; self.health = t->health; self.armour = 0; for (size_t i = 0; i < NUMGUNS; i++) self.ammo[i] = 10000; self.pitch = 0; self.roll = 0; self.state = CS_ALIVE; self.anger = 0; self.name = t->name; return self; } - (id)copy { Monster *copy = [super copy]; copy->_monsterState = _monsterState; copy->_monsterType = _monsterType; copy->_enemy = _enemy; copy->_targetYaw = _targetYaw; copy->_trigger = _trigger; copy->_attackTarget = _attackTarget; copy->_anger = _anger; return copy; } static void spawnmonster() // spawn a random monster according to freq distribution in DMSP { int n = rnd(TOTMFREQ), type; for (int i = 0;; i++) { if ((n -= monstertypes[i].freq) < 0) { type = i; break; } } [monsters addObject:[Monster monsterWithType:type yaw:rnd(360) state:M_SEARCH trigger:1000 move:1]]; } + (void)resetAll { [monsters removeAllObjects]; numkilled = 0; monstertotal = 0; spawnremain = 0; if (m_dmsp) { nextmonster = mtimestart = lastmillis + 10000; monstertotal = spawnremain = gamemode < 0 ? skill * 10 : 0; } else if (m_classicsp) { mtimestart = lastmillis; for (Entity *e in ents) { if (e.type != MONSTER) continue; Monster *m = [Monster monsterWithType:e.attr2 yaw:e.attr1 state:M_SLEEP trigger:100 move:0]; m.origin = OFMakeVector3D(e.x, e.y, e.z); [monsters addObject:m]; entinmap(m); monstertotal++; } } } // height-correct line of sight for monster shooting/seeing static bool los(float lx, float ly, float lz, float bx, float by, float bz, OFVector3D *v) { if (OUTBORD((int)lx, (int)ly) || OUTBORD((int)bx, (int)by)) return false; float dx = bx - lx; float dy = by - ly; int steps = (int)(sqrt(dx * dx + dy * dy) / 0.9); if (!steps) return false; float x = lx; float y = ly; int i = 0; for (;;) { struct sqr *s = S((int)x, (int)y); if (SOLID(s)) break; float floor = s->floor; if (s->type == FHF) floor -= s->vdelta / 4.0f; float ceil = s->ceil; if (s->type == CHF) ceil += s->vdelta / 4.0f; float rz = lz - ((lz - bz) * (i / (float)steps)); if (rz < floor || rz > ceil) break; v->x = x; v->y = y; v->z = rz; x += dx / (float)steps; y += dy / (float)steps; i++; } return i >= steps; } static bool enemylos(Monster *m, OFVector3D *v) { *v = m.origin; return los(m.origin.x, m.origin.y, m.origin.z, m.enemy.origin.x, m.enemy.origin.y, m.enemy.origin.z, v); } // 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)transitionWithState:(int)state moving:(int)moving n:(int)n r:(int)r { self.monsterState = state; self.move = moving; n = n * 130 / 100; self.trigger = lastmillis + n - skill * (n / 16) + rnd(r + 1); } - (void)normalizeWithAngle:(float)angle { while (self.yaw < angle - 180.0f) self.yaw += 360.0f; while (self.yaw > angle + 180.0f) self.yaw -= 360.0f; } // main AI thinking routine, called every frame for every monster - (void)performAction { if (self.enemy.state == CS_DEAD) { self.enemy = player1; self.anger = 0; } [self normalizeWithAngle:self.targetYaw]; // slowly turn monster towards his target if (self.targetYaw > self.yaw) { self.yaw += curtime * 0.5f; if (self.targetYaw < self.yaw) self.yaw = self.targetYaw; } else { self.yaw -= curtime * 0.5f; if (self.targetYaw > self.yaw) self.yaw = self.targetYaw; } float disttoenemy = OFDistanceOfVectors3D(self.origin, self.enemy.origin); self.pitch = atan2(self.enemy.origin.z - self.origin.z, disttoenemy) * 180 / PI; // special case: if we run into scenery if (self.blocked) { self.blocked = false; // try to jump over obstackle (rare) if (!rnd(20000 / monstertypes[self.monsterType].speed)) self.jumpNext = true; // search for a way around (common) else if (self.trigger < lastmillis && (self.monsterState != M_HOME || !rnd(5))) { // patented "random walk" AI pathfinding (tm) ;) self.targetYaw += 180 + rnd(180); [self transitionWithState:M_SEARCH moving:1 n:400 r:1000]; } } float enemyYaw = -(float)atan2(self.enemy.origin.x - self.origin.x, self.enemy.origin.y - self.origin.y) / PI * 180 + 180; switch (self.monsterState) { case M_PAIN: case M_ATTACKING: case M_SEARCH: if (self.trigger < lastmillis) [self transitionWithState:M_HOME moving:1 n:100 r:200]; break; case M_SLEEP: // state classic sp monster start in, wait for visual // contact { OFVector3D target; if (editmode || !enemylos(self, &target)) return; // skip running physics [self normalizeWithAngle:enemyYaw]; float angle = (float)fabs(enemyYaw - self.yaw); if (disttoenemy < 8 // the better the angle to the player, the // further the monster can see/hear || (disttoenemy < 16 && angle < 135) || (disttoenemy < 32 && angle < 90) || (disttoenemy < 64 && angle < 45) || angle < 10) { [self transitionWithState:M_HOME moving:1 n:500 r:200]; OFVector3D loc = self.origin; playsound(S_GRUNT1 + rnd(2), &loc); } break; } case M_AIMING: // this state is the delay between wanting to shoot and actually // firing if (self.trigger < lastmillis) { self.lastAction = 0; self.attacking = true; shoot(self, self.attackTarget); [self transitionWithState:M_ATTACKING moving:0 n:600 r:0]; } break; case M_HOME: // monster has visual contact, heads straight for player and // may want to shoot at any time self.targetYaw = enemyYaw; if (self.trigger < lastmillis) { OFVector3D target; if (!enemylos(self, &target)) { // no visual contact anymore, let monster get // as close as possible then search for player [self transitionWithState:M_HOME moving:1 n:800 r:500]; } else { // the closer the monster is the more likely he // wants to shoot if (!rnd((int)disttoenemy / 3 + 1) && self.enemy.state == CS_ALIVE) { // get ready to fire self.attackTarget = target; int n = monstertypes[self.monsterType].lag; [self transitionWithState:M_AIMING moving:0 n:n r:10]; } else { // track player some more int n = monstertypes[self.monsterType].rate; [self transitionWithState:M_HOME moving:1 n:n r:0]; } } } break; } moveplayer(self, 1, false); // use physics to move monster } - (void)incurDamage:(int)damage fromEntity:(__kindof DynamicEntity *)d { // a monster hit us if ([d isKindOfClass:Monster.class]) { Monster *m = (Monster *)d; // guard for RL guys shooting themselves :) if (self != m) { // don't attack straight away, first get angry self.anger++; int anger = (self.monsterType == m.monsterType ? self.anger / 2 : self.anger); if (anger >= monstertypes[self.monsterType].loyalty) // monster infight if very angry self.enemy = m; } } else { // player hit us self.anger = 0; self.enemy = d; } // in this state monster won't attack [self transitionWithState:M_PAIN moving:0 n:monstertypes[self.monsterType].pain r:200]; if ((self.health -= damage) <= 0) { self.state = CS_DEAD; self.lastAction = lastmillis; numkilled++; player1.frags = numkilled; OFVector3D loc = self.origin; playsound(monstertypes[self.monsterType].diesound, &loc); int remain = monstertotal - numkilled; if (remain > 0 && remain <= 5) conoutf(@"only %d monster(s) remaining", remain); } else { OFVector3D loc = self.origin; playsound(monstertypes[self.monsterType].painsound, &loc); } } + (void)endSinglePlayerWithAllKilled:(bool)allKilled { conoutf(allKilled ? @"you have cleared the map!" : @"you reached the exit!"); conoutf(@"score: %d kills in %d seconds", numkilled, (lastmillis - mtimestart) / 1000); monstertotal = 0; startintermission(); } + (void)thinkAll { if (m_dmsp && spawnremain && lastmillis > nextmonster) { if (spawnremain-- == monstertotal) conoutf(@"The invasion has begun!"); nextmonster = lastmillis + 1000; spawnmonster(); } if (monstertotal && !spawnremain && numkilled == monstertotal) [self endSinglePlayerWithAllKilled:true]; // equivalent of player entity touch, but only teleports are used [ents enumerateObjectsUsingBlock:^(Entity *e, size_t i, bool *stop) { if (e.type != TELEPORT) return; if (OUTBORD(e.x, e.y)) return; OFVector3D v = OFMakeVector3D(e.x, e.y, (float)S(e.x, e.y)->floor); for (Monster *monster in monsters) { if (monster.state == CS_DEAD) { if (lastmillis - monster.lastAction < 2000) { monster.move = 0; moveplayer(monster, 1, false); } } else { v.z += monster.eyeHeight; float dist = OFDistanceOfVectors3D(monster.origin, v); v.z -= monster.eyeHeight; if (dist < 4) teleport(i, monster); } } }]; for (Monster *monster in monsters) if (monster.state == CS_ALIVE) [monster performAction]; } + (void)renderAll { for (Monster *monster in monsters) renderclient(monster, false, monstertypes[monster.monsterType].mdlname, monster.monsterType == 5, monstertypes[monster.monsterType].mscale / 10.0f); } @end