Index: src/DynamicEntity.h ================================================================== --- src/DynamicEntity.h +++ src/DynamicEntity.h @@ -27,28 +27,14 @@ @property (nonatomic) int state; @property (nonatomic) int frags; @property (nonatomic) int health, armour, armourType, quadMillis; @property (nonatomic) int gunSelect, gunWait; @property (nonatomic) int lastAction, lastAttackGun, lastMove; -@property (nonatomic) bool attacking; @property (readonly, nonatomic) int *ammo; -// one of M_* below, M_NONE means human -@property (nonatomic) int monsterState; -// see monster.m -@property (nonatomic) int monsterType; -// monster wants to kill this entity -@property (nonatomic) DynamicEntity *enemy; -// monster wants to look in this direction -@property (nonatomic) float targetYaw; +@property (nonatomic) bool attacking; // used by physics to signal ai @property (nonatomic) bool blocked, moving; -// millis at which transition to another monsterstate takes place -@property (nonatomic) int trigger; -// delayed attacks -@property (nonatomic) OFVector3D attackTarget; -// how many times already hit by fellow monster -@property (nonatomic) int anger; @property (copy, nonatomic) OFString *name, *team; + (instancetype)entity; - (OFData *)dataBySerializing; - (void)setFromSerializedData:(OFData *)data; Index: src/DynamicEntity.m ================================================================== --- src/DynamicEntity.m +++ src/DynamicEntity.m @@ -1,9 +1,11 @@ #import "DynamicEntity.h" #include "cube.h" +#import "Monster.h" + struct dynent { OFVector3D origin, velocity; float yaw, pitch, roll; float maxSpeed; bool outsideMap; @@ -111,20 +113,12 @@ copy->_attacking = _attacking; for (size_t i = 0; i < NUMGUNS; i++) copy->_ammo[i] = _ammo[i]; - copy->_monsterState = _monsterState; - copy->_monsterType = _monsterType; - copy->_enemy = _enemy; - copy->_targetYaw = _targetYaw; copy->_blocked = _blocked; copy->_moving = _moving; - copy->_trigger = _trigger; - copy->_attackTarget = _attackTarget; - copy->_anger = _anger; - copy->_name = [_name copy]; copy->_team = [_team copy]; return copy; } @@ -167,18 +161,22 @@ .gunWait = _gunWait, .lastAction = _lastAction, .lastAttackGun = _lastAttackGun, .lastMove = _lastMove, .attacking = _attacking, - .monsterState = _monsterState, - .monsterType = _monsterType, - .targetYaw = _targetYaw, .blocked = _blocked, - .moving = _moving, - .trigger = _trigger, - .attackTarget = _attackTarget, - .anger = _anger }; + .moving = _moving }; + + if ([self isKindOfClass:Monster.class]) { + Monster *monster = (Monster *)self; + data.monsterState = monster.monsterState; + data.monsterType = monster.monsterType; + data.targetYaw = monster.targetYaw; + data.trigger = monster.trigger; + data.attackTarget = monster.attackTarget; + data.anger = monster.anger; + } for (int i = 0; i < NUMGUNS; i++) data.ammo[i] = _ammo[i]; memcpy(data.name, _name.UTF8String, min(_name.UTF8StringLength, 259)); @@ -234,18 +232,22 @@ _attacking = d.attacking; for (int i = 0; i < NUMGUNS; i++) _ammo[i] = d.ammo[i]; - _monsterState = d.monsterState; - _monsterType = d.monsterType; - _targetYaw = d.targetYaw; _blocked = d.blocked; _moving = d.moving; - _trigger = d.trigger; - _attackTarget = d.attackTarget; - _anger = d.anger; + + if ([self isKindOfClass:Monster.class]) { + Monster *monster = (Monster *)self; + monster.monsterState = d.monsterState; + monster.monsterType = d.monsterType; + monster.targetYaw = d.targetYaw; + monster.trigger = d.trigger; + monster.attackTarget = d.attackTarget; + monster.anger = d.anger; + } _name = [[OFString alloc] initWithUTF8String:d.name]; _team = [[OFString alloc] initWithUTF8String:d.team]; } ADDED src/Monster.h Index: src/Monster.h ================================================================== --- /dev/null +++ src/Monster.h @@ -0,0 +1,39 @@ +#import "DynamicEntity.h" + +@interface Monster: DynamicEntity +@property (class, readonly, nonatomic) OFMutableArray *monsters; +// one of M_* +@property (nonatomic) int monsterState; +// see Monster.m +@property (nonatomic) int monsterType; +// monster wants to kill this entity +@property (nonatomic) DynamicEntity *enemy; +// monster wants to look in this direction +@property (nonatomic) float targetYaw; +// millis at which transition to another monsterstate takes place +@property (nonatomic) int trigger; +// delayed attacks +@property (nonatomic) OFVector3D attackTarget; +// how many times already hit by fellow monster +@property (nonatomic) int anger; + +// called after map start of when toggling edit mode to reset/spawn all +// monsters to initial state ++ (void)restoreAll; ++ (void)resetAll; ++ (void)thinkAll; ++ (void)renderAll; +// TODO: Move this somewhere else ++ (void)endSinglePlayerWithAllKilled:(bool)allKilled; ++ (instancetype)monsterWithType:(int)type + yaw:(int)yaw + state:(int)state + trigger:(int)trigger + move:(int)move; +- (instancetype)initWithType:(int)type + yaw:(int)yaw + state:(int)state + trigger:(int)trigger + move:(int)move; +- (void)incurDamage:(int)damage fromEntity:(__kindof DynamicEntity *)d; +@end ADDED src/Monster.m Index: src/Monster.m ================================================================== --- /dev/null +++ src/Monster.m @@ -0,0 +1,494 @@ +// 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 *monsters; +static int nextmonster, spawnremain, numkilled, monstertotal, mtimestart; + +@implementation Monster ++ (void)initialize +{ + monsters = [[OFMutableArray alloc] init]; +} + ++ (OFMutableArray *)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(fast_f2nat(x), fast_f2nat(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; + } + + vdist(disttoenemy, vectoenemy, 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; + OFVector3D attackTarget = self.attackTarget; + shoot(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; + vdist(dist, t, 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 Index: src/clientextras.m ================================================================== --- src/clientextras.m +++ src/clientextras.m @@ -1,10 +1,11 @@ // clientextras.cpp: stuff that didn't fit in client.cpp or clientgame.cpp :) #include "cube.h" #import "DynamicEntity.h" +#import "Monster.h" // render players & monsters // very messy ad-hoc handling of animation frames, should be made more // configurable @@ -51,13 +52,15 @@ // scale = 1.2f; } else if (d.state == CS_EDITING) { n = 16; } else if (d.state == CS_LAGGED) { n = 17; - } else if (d.monsterState == M_ATTACKING) { + } else if ([d isKindOfClass:Monster.class] && + ((Monster *)d).monsterState == M_ATTACKING) { n = 8; - } else if (d.monsterState == M_PAIN) { + } else if ([d isKindOfClass:Monster.class] && + ((Monster *)d).monsterState == M_PAIN) { n = 10; } else if ((!d.move && !d.strafe) || !d.moving) { n = 12; } else if (!d.onFloor && d.timeInAir > 100) { n = 18; Index: src/clientgame.m ================================================================== --- src/clientgame.m +++ src/clientgame.m @@ -2,10 +2,11 @@ #include "cube.h" #import "DynamicEntity.h" #import "Entity.h" +#import "Monster.h" #import "OFString+Cube.h" int nextmode = 0; // nextmode becomes gamemode after next map load VAR(gamemode, 1, 0, 0); @@ -172,11 +173,11 @@ // when our player moves gets2c(); } otherplayers(); if (!demoplayback) { - monsterthink(); + [Monster thinkAll]; if (player1.state == CS_DEAD) { if (lastmillis - player1.lastAction < 2000) { player1.move = player1.strafe = 0; moveplayer(player1, 10, false); } else if (!m_arena && !m_sp && @@ -415,11 +416,11 @@ if (netmapstart() && m_sp) { gamemode = 0; conoutf(@"coop sp not supported yet"); } sleepwait = 0; - monsterclear(); + [Monster resetAll]; projreset(); spawncycle = -1; spawnplayer(player1); player1.frags = 0; for (id player in players) Index: src/cube.h ================================================================== --- src/cube.h +++ src/cube.h @@ -124,11 +124,10 @@ // bump if dynent/netprotocol changes or any other savegame/demo data #define SAVEGAMEVERSION 4 enum { A_BLUE, A_GREEN, A_YELLOW }; // armour types... take 20/40/60 % off enum { - M_NONE = 0, M_SEARCH, M_HOME, M_ATTACKING, M_PAIN, M_SLEEP, Index: src/editing.m ================================================================== --- src/editing.m +++ src/editing.m @@ -2,10 +2,11 @@ // in world.cpp #include "cube.h" #import "DynamicEntity.h" +#import "Monster.h" #import "OFString+Cube.h" bool editmode = false; // the current selection, used by almost all editing commands @@ -66,12 +67,12 @@ } else { resettagareas(); // clear trigger areas to allow them to be // edited player1.health = 100; if (m_classicsp) - monsterclear(); // all monsters back at their spawns for - // editing + // all monsters back at their spawns for editing + [Monster resetAll]; projreset(); } Cube.sharedInstance.repeatsKeys = editmode; selset = false; editing = editmode; Index: src/meson.build ================================================================== --- src/meson.build +++ src/meson.build @@ -11,10 +11,11 @@ 'KeyMapping.m', 'MD2.m', 'MapModelInfo.m', 'Menu.m', 'MenuItem.m', + 'Monster.m', 'OFString+Cube.m', 'Projectile.m', 'ResolverResult.m', 'ResolverThread.m', 'ServerEntity.m', @@ -28,11 +29,10 @@ 'console.m', 'editing.m', 'entities.m', 'init.m', 'menus.m', - 'monster.m', 'physics.m', 'rendercubes.m', 'renderextras.m', 'rendergl.m', 'rendermd2.m', DELETED src/monster.m Index: src/monster.m ================================================================== --- src/monster.m +++ /dev/null @@ -1,428 +0,0 @@ -// monster.cpp: implements AI for single player monsters, currently client only - -#include "cube.h" - -#import "DynamicEntity.h" -#import "Entity.h" - -static OFMutableArray *monsters; -static int nextmonster, spawnremain, numkilled, monstertotal, mtimestart; - -VARF(skill, 1, 3, 10, conoutf(@"skill is now %d", skill)); - -OFArray * -getmonsters() -{ - return monsters; -} - -// for savegames -void -restoremonsterstate() -{ - for (DynamicEntity *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" }, -}; - -DynamicEntity * -basicmonster(int type, int yaw, int state, int trigger, int move) -{ - if (type >= NUMMONSTERTYPES) { - conoutf(@"warning: unknown monster in spawn: %d", type); - type = 0; - } - DynamicEntity *m = [DynamicEntity entity]; - struct monstertype *t = &monstertypes[(m.monsterType = type)]; - m.eyeHeight = 2.0f; - m.aboveEye = 1.9f; - m.radius *= t->bscale / 10.0f; - m.eyeHeight *= t->bscale / 10.0f; - m.aboveEye *= t->bscale / 10.0f; - m.monsterState = state; - if (state != M_SLEEP) - spawnplayer(m); - m.trigger = lastmillis + trigger; - m.targetYaw = m.yaw = (float)yaw; - m.move = move; - m.enemy = player1; - m.gunSelect = t->gun; - m.maxSpeed = (float)t->speed; - m.health = t->health; - m.armour = 0; - loopi(NUMGUNS) m.ammo[i] = 10000; - m.pitch = 0; - m.roll = 0; - m.state = CS_ALIVE; - m.anger = 0; - m.name = t->name; - - if (monsters == nil) - monsters = [[OFMutableArray alloc] init]; - - [monsters addObject:m]; - - return m; -} - -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; - } - } - basicmonster(type, rnd(360), M_SEARCH, 1000, 1); -} - -// called after map start of when toggling edit mode to reset/spawn all -// monsters to initial state -void -monsterclear() -{ - [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; - - DynamicEntity *m = - basicmonster(e.attr2, e.attr1, M_SLEEP, 100, 0); - m.origin = OFMakeVector3D(e.x, e.y, e.z); - entinmap(m); - monstertotal++; - } - } -} - -// height-correct line of sight for monster shooting/seeing -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(fast_f2nat(x), fast_f2nat(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; -} - -bool -enemylos(DynamicEntity *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 -transition(DynamicEntity *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); -} - -void -normalise(DynamicEntity *m, float angle) -{ - while (m.yaw < angle - 180.0f) - m.yaw += 360.0f; - while (m.yaw > angle + 180.0f) - m.yaw -= 360.0f; -} - -// main AI thinking routine, called every frame for every monster -void -monsteraction(DynamicEntity *m) -{ - if (m.enemy.state == CS_DEAD) { - m.enemy = player1; - m.anger = 0; - } - normalise(m, m.targetYaw); - // slowly turn monster towards his target - if (m.targetYaw > m.yaw) { - m.yaw += curtime * 0.5f; - if (m.targetYaw < m.yaw) - m.yaw = m.targetYaw; - } else { - m.yaw -= curtime * 0.5f; - if (m.targetYaw > m.yaw) - m.yaw = m.targetYaw; - } - - vdist(disttoenemy, vectoenemy, m.origin, m.enemy.origin); - m.pitch = atan2(m.enemy.origin.z - m.origin.z, disttoenemy) * 180 / 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.monsterType].speed)) - m.jumpNext = true; - // search for a way around (common) - else if (m.trigger < lastmillis && - (m.monsterState != M_HOME || !rnd(5))) { - // patented "random walk" AI pathfinding (tm) ;) - m.targetYaw += 180 + rnd(180); - transition(m, M_SEARCH, 1, 400, 1000); - } - } - - float enemyYaw = -(float)atan2(m.enemy.origin.x - m.origin.x, - m.enemy.origin.y - m.origin.y) / - PI * 180 + - 180; - - switch (m.monsterState) { - case M_PAIN: - case M_ATTACKING: - case M_SEARCH: - if (m.trigger < lastmillis) - transition(m, M_HOME, 1, 100, 200); - break; - - case M_SLEEP: // state classic sp monster start in, wait for visual - // contact - { - OFVector3D target; - if (editmode || !enemylos(m, &target)) - return; // skip running physics - normalise(m, enemyYaw); - float angle = (float)fabs(enemyYaw - m.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) { - transition(m, M_HOME, 1, 500, 200); - OFVector3D loc = m.origin; - playsound(S_GRUNT1 + rnd(2), &loc); - } - break; - } - - case M_AIMING: - // this state is the delay between wanting to shoot and actually - // firing - if (m.trigger < lastmillis) { - m.lastAction = 0; - m.attacking = true; - OFVector3D attackTarget = m.attackTarget; - shoot(m, &attackTarget); - transition(m, M_ATTACKING, 0, 600, 0); - } - break; - - case M_HOME: - // monster has visual contact, heads straight for player and - // may want to shoot at any time - m.targetYaw = enemyYaw; - if (m.trigger < lastmillis) { - OFVector3D target; - if (!enemylos(m, &target)) { - // no visual contact anymore, let monster get - // as close as possible then search for player - 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.monsterType].lag, - 10); - } else - // track player some more - transition(m, M_HOME, 1, - monstertypes[m.monsterType].rate, - 0); - } - } - break; - } - - moveplayer(m, 1, false); // use physics to move monster -} - -void -monsterpain(DynamicEntity *m, int damage, DynamicEntity *d) -{ - // a monster hit us - if (d.monsterState) { - // guard for RL guys shooting themselves :) - if (m != d) { - // don't attack straight away, first get angry - m.anger++; - int anger = m.monsterType == d.monsterType ? m.anger / 2 - : m.anger; - if (anger >= monstertypes[m.monsterType].loyalty) - // monster infight if very angry - m.enemy = d; - } - } else { - // player hit us - m.anger = 0; - m.enemy = d; - } - // in this state monster won't attack - transition(m, M_PAIN, 0, monstertypes[m.monsterType].pain, 200); - if ((m.health -= damage) <= 0) { - m.state = CS_DEAD; - m.lastAction = lastmillis; - numkilled++; - player1.frags = numkilled; - OFVector3D loc = m.origin; - playsound(monstertypes[m.monsterType].diesound, &loc); - int remain = monstertotal - numkilled; - if (remain > 0 && remain <= 5) - conoutf(@"only %d monster(s) remaining", remain); - } else { - OFVector3D loc = m.origin; - playsound(monstertypes[m.monsterType].painsound, &loc); - } -} - -void -endsp(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 -monsterthink() -{ - if (m_dmsp && spawnremain && lastmillis > nextmonster) { - if (spawnremain-- == monstertotal) - conoutf(@"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 - [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 (DynamicEntity *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; - vdist(dist, t, monster.origin, v); - v.z -= monster.eyeHeight; - - if (dist < 4) - teleport(i, monster); - } - } - }]; - - for (DynamicEntity *monster in monsters) - if (monster.state == CS_ALIVE) - monsteraction(monster); -} - -void -monsterrender() -{ - for (DynamicEntity *monster in monsters) - renderclient(monster, false, - monstertypes[monster.monsterType].mdlname, - monster.monsterType == 5, - monstertypes[monster.monsterType].mscale / 10.0f); -} Index: src/physics.m ================================================================== --- src/physics.m +++ src/physics.m @@ -7,10 +7,11 @@ #include "cube.h" #import "DynamicEntity.h" #import "Entity.h" #import "MapModelInfo.h" +#import "Monster.h" // collide with player or monster static bool plcollide( DynamicEntity *d, DynamicEntity *o, float *headspace, float *hi, float *lo) @@ -27,13 +28,13 @@ } else if (o.origin.z + o.aboveEye > *lo) *lo = o.origin.z + o.aboveEye + 1; if (fabs(o.origin.z - d.origin.z) < o.aboveEye + d.eyeHeight) return false; - if (d.monsterState) + if ([d isKindOfClass:Monster.class]) return false; // hack - // + *headspace = d.origin.z - o.origin.z - o.aboveEye - d.eyeHeight; if (*headspace < 0) *headspace = 10; } @@ -103,13 +104,14 @@ const int y1 = fast_f2nat(fy1); const int x2 = fast_f2nat(fx2); const int y2 = fast_f2nat(fy2); float hi = 127, lo = -128; // big monsters are afraid of heights, unless angry :) - float minfloor = (d.monsterState && !spawn && d.health > 100) - ? d.origin.z - d.eyeHeight - 4.5f - : -1000.0f; + float minfloor = + ([d isKindOfClass:Monster.class] && !spawn && d.health > 100 + ? d.origin.z - d.eyeHeight - 4.5f + : -1000.0f); for (int x = x1; x <= x2; x++) { for (int y = y1; y <= y2; y++) { // collide with map if (OUTBORD(x, y)) @@ -181,11 +183,11 @@ if (!plcollide(d, player1, &headspace, &hi, &lo)) return false; // this loop can be a performance bottleneck with many monster on a slow // cpu, should replace with a blockmap but seems mostly fast enough - for (DynamicEntity *monster in getmonsters()) + for (Monster *monster in Monster.monsters) if (!vreject(d.origin, monster.origin, 7.0f) && d != monster && !plcollide(d, monster, &headspace, &hi, &lo)) return false; headspace -= 0.01f; @@ -321,20 +323,20 @@ pl.velocity = OFMakeVector3D( pl.velocity.x / 8, pl.velocity.y / 8, pl.velocity.z); if (local) playsoundc(S_JUMP); - else if (pl.monsterState) { + else if ([pl isKindOfClass:Monster.class]) { OFVector3D loc = pl.origin; playsound(S_JUMP, &loc); } } else if (pl.timeInAir > 800) { // if we land after long time must have been a // high jump, make thud sound if (local) playsoundc(S_LAND); - else if (pl.monsterState) { + else if ([pl isKindOfClass:Monster.class]) { OFVector3D loc = pl.origin; playsound(S_LAND, &loc); } } Index: src/protos.h ================================================================== --- src/protos.h +++ src/protos.h @@ -248,19 +248,10 @@ extern void moveprojectiles(float time); extern void projreset(); extern OFString *playerincrosshair(); extern int reloadtime(int gun); -// monster -extern void monsterclear(); -extern void restoremonsterstate(); -extern void monsterthink(); -extern void monsterrender(); -extern OFArray *getmonsters(); -extern void monsterpain(DynamicEntity *m, int damage, DynamicEntity *d); -extern void endsp(bool allkilled); - // entities extern void initEntities(); extern void renderents(); extern void putitems(uchar **p); extern void checkquad(int time); Index: src/rendergl.m ================================================================== --- src/rendergl.m +++ src/rendergl.m @@ -1,10 +1,11 @@ // rendergl.cpp: core opengl rendering stuff #include "cube.h" #import "DynamicEntity.h" +#import "Monster.h" #import "OFString+Cube.h" #ifdef DARWIN # define GL_COMBINE_EXT GL_COMBINE_ARB # define GL_COMBINE_RGB_EXT GL_COMBINE_RGB_ARB @@ -476,11 +477,11 @@ renderstrips(); xtraverts = 0; renderclients(); - monsterrender(); + [Monster renderAll]; renderentities(); renderspheres(curtime); renderents(); Index: src/savegamedemo.m ================================================================== --- src/savegamedemo.m +++ src/savegamedemo.m @@ -3,10 +3,11 @@ #include "cube.h" #import "DynamicEntity.h" #import "Entity.h" +#import "Monster.h" #ifdef OF_BIG_ENDIAN static const int islittleendian = 0; #else static const int islittleendian = 1; @@ -112,13 +113,13 @@ gzputi(gamemode); gzputi(ents.count); for (Entity *e in ents) gzputc(f, e.spawned); gzwrite(f, data.items, data.count); - OFArray *monsters = getmonsters(); + OFArray *monsters = Monster.monsters; gzputi(monsters.count); - for (DynamicEntity *monster in monsters) { + for (Monster *monster in monsters) { data = [monster dataBySerializing]; gzwrite(f, data.items, data.count); } gzputi(players.count); for (id player in players) { @@ -225,25 +226,25 @@ gzread(f, data.mutableItems, data.count); [player1 setFromSerializedData:data]; player1.lastAction = lastmillis; int nmonsters = gzgeti(); - OFArray *monsters = getmonsters(); + OFArray *monsters = Monster.monsters; if (nmonsters != monsters.count) return loadgameout(); - for (DynamicEntity *monster in monsters) { + for (Monster *monster in monsters) { gzread(f, data.mutableItems, data.count); [monster setFromSerializedData:data]; // lazy, could save id of enemy instead monster.enemy = player1; // also lazy, but no real noticable effect on game monster.lastAction = monster.trigger = lastmillis + 500; if (monster.state == CS_DEAD) monster.lastAction = 0; } - restoremonsterstate(); + [Monster restoreAll]; int nplayers = gzgeti(); loopi(nplayers) if (!gzget()) { DynamicEntity *d = getclient(i); Index: src/weapon.m ================================================================== --- src/weapon.m +++ src/weapon.m @@ -1,10 +1,11 @@ // weapon.cpp: all shooting and effects code #include "cube.h" #import "DynamicEntity.h" +#import "Monster.h" #import "OFString+Cube.h" #import "Projectile.h" static const int MONSTERDAMAGEFACTOR = 4; #define SGRAYS 20 @@ -166,17 +167,17 @@ return; } } void -hit(int target, int damage, DynamicEntity *d, DynamicEntity *at) +hit(int target, int damage, __kindof DynamicEntity *d, DynamicEntity *at) { OFVector3D o = d.origin; if (d == player1) selfdamage(damage, at == player1 ? -1 : -2, at); - else if (d.monsterState) - monsterpain(d, damage, at); + else if ([d isKindOfClass:Monster.class]) + [d incurDamage:damage fromEntity:at]; else { addmsg(1, 4, SV_DAMAGE, target, damage, d.lifeSequence); playsound(S_PAIN1 + rnd(5), &o); } particle_splash(3, damage, 1000, &o); @@ -233,12 +234,12 @@ return; radialeffect(player, v, i, qdam, p.owner); }]; - [getmonsters() enumerateObjectsUsingBlock:^( - DynamicEntity *monster, size_t i, bool *stop) { + [Monster.monsters enumerateObjectsUsingBlock:^( + Monster *monster, size_t i, bool *stop) { if (i != notthismonster) radialeffect(monster, v, i, qdam, p.owner); }]; } } @@ -265,11 +266,11 @@ if (!p.inuse) continue; int qdam = guns[p.gun].damage * (p.owner.quadMillis ? 4 : 1); - if (p.owner.monsterState) + if ([p.owner isKindOfClass:Monster.class]) qdam /= MONSTERDAMAGEFACTOR; vdist(dist, v, p.o, p.to); float dtime = dist * 1000 / p.speed; if (time > dtime) dtime = time; @@ -281,11 +282,11 @@ projdamage(player, p, &v, i, -1, qdam); if (p.owner != player1) projdamage(player1, p, &v, -1, -1, qdam); - for (DynamicEntity *monster in getmonsters()) + for (Monster *monster in Monster.monsters) if (!vreject(monster.origin, v, 10.0f) && monster != p.owner) projdamage(monster, p, &v, -1, i, qdam); } if (p.inuse) { @@ -333,11 +334,11 @@ case GUN_RL: case GUN_FIREBALL: case GUN_ICEBALL: case GUN_SLIMEBALL: pspeed = guns[gun].projspeed; - if (d.monsterState) + if ([d isKindOfClass:Monster.class]) pspeed /= 2; newprojectile(from, to, (float)pspeed, local, d, gun); break; case GUN_RIFLE: @@ -364,11 +365,11 @@ if (o.state != CS_ALIVE) return; int qdam = guns[d.gunSelect].damage; if (d.quadMillis) qdam *= 4; - if (d.monsterState) + if ([d isKindOfClass:Monster.class]) qdam /= MONSTERDAMAGEFACTOR; if (d.gunSelect == GUN_SG) { int damage = 0; loop(r, SGRAYS) if (intersect(o, from, &sg[r])) damage += qdam; if (damage) @@ -417,11 +418,11 @@ createrays(&from, &to); if (d.quadMillis && attacktime > 200) playsoundc(S_ITEMPUP); shootv(d.gunSelect, &from, &to, d, true); - if (!d.monsterState) + if (![d isKindOfClass:Monster.class]) addmsg(1, 8, SV_SHOT, d.gunSelect, (int)(from.x * DMF), (int)(from.y * DMF), (int)(from.z * DMF), (int)(to.x * DMF), (int)(to.y * DMF), (int)(to.z * DMF)); d.gunWait = guns[d.gunSelect].attackdelay; @@ -431,12 +432,12 @@ [players enumerateObjectsUsingBlock:^(id player, size_t i, bool *stop) { if (player != [OFNull null]) raydamage(player, &from, &to, d, i); }]; - for (DynamicEntity *monster in getmonsters()) + for (Monster *monster in Monster.monsters) if (monster != d) raydamage(monster, &from, &to, d, -2); - if (d.monsterState) + if ([d isKindOfClass:Monster.class]) raydamage(player1, &from, &to, d, -1); } Index: src/world.m ================================================================== --- src/world.m +++ src/world.m @@ -2,10 +2,11 @@ #include "cube.h" #import "DynamicEntity.h" #import "Entity.h" +#import "Monster.h" extern OFString *entnames[]; // lookup from map entities above to strings struct sqr *world = NULL; int sfactor, ssize, cubicsize, mipsize; @@ -77,11 +78,11 @@ if (identexists(aliasname)) execute(aliasname, true); if (type == 2) - endsp(false); + [Monster endSinglePlayerWithAllKilled:false]; } COMMAND(trigger, ARG_2INT) // main geometric mipmapping routine, recursively rebuild mipmaps within block // b. tries to produce cube out of 4 lower level mips as well as possible, sets Index: src/worldlight.m ================================================================== --- src/worldlight.m +++ src/worldlight.m @@ -2,10 +2,11 @@ #include "cube.h" #import "DynamicEntity.h" #import "Entity.h" +#import "Monster.h" extern bool hasoverbright; VAR(lightscale, 1, 4, 100); @@ -203,11 +204,11 @@ dodynlight(const OFVector3D *vold, const OFVector3D *v, int reach, int strength, DynamicEntity *owner) { if (!reach) reach = dynlight; - if (owner.monsterState) + if ([owner isKindOfClass:Monster.class]) reach = reach / 2; if (!reach) return; if (v->x < 0 || v->y < 0 || v->x > ssize || v->y > ssize) return;