Artifact 911d4fb8557f8c8ac1e1fe73e67d2d4b389081bc782cb71764219f88cd2173d1:
- File
src/Monster.m
— part of check-in
[75e920ae30]
at
2025-03-29 14:25:43
on branch trunk
— Switch from clang-format to manual formatting
clang-format does too many weird things. (user: js, size: 13378) [annotate] [blame] [check-ins using]
// monster.cpp: implements AI for single player monsters, currently client only #import "Monster.h" #include "cube.h" #import "Entity.h" #import "Player.h" #import "Variable.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 = Player.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 = Player.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.enemy.origin, self.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++; Player.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(v, monster.origin); 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