Convert monster into a class

FossilOrigin-Name: e8f80b0482846dfbd5bf97ed375b13687d325e9ef7e2a79176dac49722180124
This commit is contained in:
Jonathan Schleifer 2025-03-23 02:03:32 +00:00
parent a7db00c740
commit 2c5f2d0342
16 changed files with 319 additions and 224 deletions

View file

@ -29,24 +29,10 @@
@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;

View file

@ -2,6 +2,8 @@
#include "cube.h"
#import "Monster.h"
struct dynent {
OFVector3D origin, velocity;
float yaw, pitch, roll;
@ -113,16 +115,8 @@ struct dynent {
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];
@ -169,14 +163,18 @@ struct dynent {
.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];
@ -236,14 +234,18 @@ struct dynent {
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];

39
src/Monster.h Normal file
View file

@ -0,0 +1,39 @@
#import "DynamicEntity.h"
@interface Monster: DynamicEntity
@property (class, readonly, nonatomic) OFMutableArray<Monster *> *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

View file

@ -1,26 +1,45 @@
// 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<DynamicEntity *> *monsters;
static OFMutableArray<Monster *> *monsters;
static int nextmonster, spawnremain, numkilled, monstertotal, mtimestart;
VARF(skill, 1, 3, 10, conoutf(@"skill is now %d", skill));
@implementation Monster
+ (void)initialize
{
monsters = [[OFMutableArray alloc] init];
}
OFArray<DynamicEntity *> *
getmonsters()
+ (OFMutableArray<Monster *> *)monsters
{
return monsters;
}
// for savegames
void
restoremonsterstate()
+ (instancetype)monsterWithType:(int)type
yaw:(int)yaw
state:(int)state
trigger:(int)trigger
move:(int)move
{
for (DynamicEntity *monster in monsters)
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++;
}
@ -55,47 +74,67 @@ monstertypes[NUMMONSTERTYPES] = {
@"a goblin", @"monster/goblin" },
};
DynamicEntity *
basicmonster(int type, int yaw, int state, int trigger, int move)
- (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;
}
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;
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(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;
spawnplayer(self);
if (monsters == nil)
monsters = [[OFMutableArray alloc] init];
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;
[monsters addObject:m];
for (size_t i = 0; i < NUMGUNS; i++)
self.ammo[i] = 10000;
return m;
self.pitch = 0;
self.roll = 0;
self.state = CS_ALIVE;
self.anger = 0;
self.name = t->name;
return self;
}
void
- (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;
@ -105,18 +144,22 @@ spawnmonster() // spawn a random monster according to freq distribution in DMSP
break;
}
}
basicmonster(type, rnd(360), M_SEARCH, 1000, 1);
[monsters addObject:[Monster monsterWithType:type
yaw:rnd(360)
state:M_SEARCH
trigger:1000
move:1]];
}
// called after map start of when toggling edit mode to reset/spawn all
// monsters to initial state
void
monsterclear()
+ (void)resetAll
{
[monsters removeAllObjects];
numkilled = 0;
monstertotal = 0;
spawnremain = 0;
if (m_dmsp) {
nextmonster = mtimestart = lastmillis + 10000;
monstertotal = spawnremain = gamemode < 0 ? skill * 10 : 0;
@ -127,9 +170,13 @@ monsterclear()
if (e.type != MONSTER)
continue;
DynamicEntity *m =
basicmonster(e.attr2, e.attr1, M_SLEEP, 100, 0);
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++;
}
@ -137,7 +184,7 @@ monsterclear()
}
// height-correct line of sight for monster shooting/seeing
bool
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))
@ -173,8 +220,8 @@ los(float lx, float ly, float lz, float bx, float by, float bz, OFVector3D *v)
return i >= steps;
}
bool
enemylos(DynamicEntity *m, OFVector3D *v)
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,
@ -189,90 +236,91 @@ enemylos(DynamicEntity *m, OFVector3D *v)
// 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)
- (void)transitionWithState:(int)state moving:(int)moving n:(int)n r:(int)r
{
m.monsterState = state;
m.move = moving;
self.monsterState = state;
self.move = moving;
n = n * 130 / 100;
m.trigger = lastmillis + n - skill * (n / 16) + rnd(r + 1);
self.trigger = lastmillis + n - skill * (n / 16) + rnd(r + 1);
}
void
normalise(DynamicEntity *m, float angle)
- (void)normalizeWithAngle:(float)angle
{
while (m.yaw < angle - 180.0f)
m.yaw += 360.0f;
while (m.yaw > angle + 180.0f)
m.yaw -= 360.0f;
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
monsteraction(DynamicEntity *m)
- (void)performAction
{
if (m.enemy.state == CS_DEAD) {
m.enemy = player1;
m.anger = 0;
if (self.enemy.state == CS_DEAD) {
self.enemy = player1;
self.anger = 0;
}
normalise(m, m.targetYaw);
[self normalizeWithAngle:self.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;
if (self.targetYaw > self.yaw) {
self.yaw += curtime * 0.5f;
if (self.targetYaw < self.yaw)
self.yaw = self.targetYaw;
} else {
m.yaw -= curtime * 0.5f;
if (m.targetYaw > m.yaw)
m.yaw = m.targetYaw;
self.yaw -= curtime * 0.5f;
if (self.targetYaw > self.yaw)
self.yaw = self.targetYaw;
}
vdist(disttoenemy, vectoenemy, m.origin, m.enemy.origin);
m.pitch = atan2(m.enemy.origin.z - m.origin.z, disttoenemy) * 180 / PI;
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 (m.blocked) {
m.blocked = false;
if (self.blocked) {
self.blocked = false;
// try to jump over obstackle (rare)
if (!rnd(20000 / monstertypes[m.monsterType].speed))
m.jumpNext = true;
if (!rnd(20000 / monstertypes[self.monsterType].speed))
self.jumpNext = true;
// search for a way around (common)
else if (m.trigger < lastmillis &&
(m.monsterState != M_HOME || !rnd(5))) {
else if (self.trigger < lastmillis &&
(self.monsterState != M_HOME || !rnd(5))) {
// patented "random walk" AI pathfinding (tm) ;)
m.targetYaw += 180 + rnd(180);
transition(m, M_SEARCH, 1, 400, 1000);
self.targetYaw += 180 + rnd(180);
[self transitionWithState:M_SEARCH
moving:1
n:400
r:1000];
}
}
float enemyYaw = -(float)atan2(m.enemy.origin.x - m.origin.x,
m.enemy.origin.y - m.origin.y) /
float enemyYaw = -(float)atan2(self.enemy.origin.x - self.origin.x,
self.enemy.origin.y - self.origin.y) /
PI * 180 +
180;
switch (m.monsterState) {
switch (self.monsterState) {
case M_PAIN:
case M_ATTACKING:
case M_SEARCH:
if (m.trigger < lastmillis)
transition(m, M_HOME, 1, 100, 200);
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(m, &target))
if (editmode || !enemylos(self, &target))
return; // skip running physics
normalise(m, enemyYaw);
float angle = (float)fabs(enemyYaw - m.yaw);
[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) {
transition(m, M_HOME, 1, 500, 200);
OFVector3D loc = m.origin;
[self transitionWithState:M_HOME moving:1 n:500 r:200];
OFVector3D loc = self.origin;
playsound(S_GRUNT1 + rnd(2), &loc);
}
break;
@ -281,90 +329,109 @@ monsteraction(DynamicEntity *m)
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);
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
m.targetYaw = enemyYaw;
if (m.trigger < lastmillis) {
self.targetYaw = enemyYaw;
if (self.trigger < lastmillis) {
OFVector3D target;
if (!enemylos(m, &target)) {
if (!enemylos(self, &target)) {
// no visual contact anymore, let monster get
// as close as possible then search for player
transition(m, M_HOME, 1, 800, 500);
[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) &&
m.enemy.state == CS_ALIVE) {
self.enemy.state == CS_ALIVE) {
// get ready to fire
m.attackTarget = target;
transition(m, M_AIMING, 0,
monstertypes[m.monsterType].lag,
10);
} else
self.attackTarget = target;
int n =
monstertypes[self.monsterType].lag;
[self transitionWithState:M_AIMING
moving:0
n:n
r:10];
} else {
// track player some more
transition(m, M_HOME, 1,
monstertypes[m.monsterType].rate,
0);
int n =
monstertypes[self.monsterType].rate;
[self transitionWithState:M_HOME
moving:1
n:n
r:0];
}
}
}
break;
}
moveplayer(m, 1, false); // use physics to move monster
moveplayer(self, 1, false); // use physics to move monster
}
void
monsterpain(DynamicEntity *m, int damage, DynamicEntity *d)
- (void)incurDamage:(int)damage fromEntity:(__kindof DynamicEntity *)d
{
// a monster hit us
if (d.monsterState) {
if ([d isKindOfClass:Monster.class]) {
Monster *m = (Monster *)d;
// guard for RL guys shooting themselves :)
if (m != d) {
if (self != m) {
// 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)
self.anger++;
int anger =
(self.monsterType == m.monsterType ? self.anger / 2
: self.anger);
if (anger >= monstertypes[self.monsterType].loyalty)
// monster infight if very angry
m.enemy = d;
self.enemy = m;
}
} else {
// player hit us
m.anger = 0;
m.enemy = d;
self.anger = 0;
self.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;
[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 = m.origin;
playsound(monstertypes[m.monsterType].diesound, &loc);
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 = m.origin;
playsound(monstertypes[m.monsterType].painsound, &loc);
OFVector3D loc = self.origin;
playsound(monstertypes[self.monsterType].painsound, &loc);
}
}
void
endsp(bool allkilled)
+ (void)endSinglePlayerWithAllKilled:(bool)allKilled
{
conoutf(allkilled ? @"you have cleared the map!"
conoutf(allKilled ? @"you have cleared the map!"
: @"you reached the exit!");
conoutf(@"score: %d kills in %d seconds", numkilled,
(lastmillis - mtimestart) / 1000);
@ -372,8 +439,7 @@ endsp(bool allkilled)
startintermission();
}
void
monsterthink()
+ (void)thinkAll
{
if (m_dmsp && spawnremain && lastmillis > nextmonster) {
if (spawnremain-- == monstertotal)
@ -383,7 +449,7 @@ monsterthink()
}
if (monstertotal && !spawnremain && numkilled == monstertotal)
endsp(true);
[self endSinglePlayerWithAllKilled:true];
// equivalent of player entity touch, but only teleports are used
[ents enumerateObjectsUsingBlock:^(Entity *e, size_t i, bool *stop) {
@ -395,7 +461,7 @@ monsterthink()
OFVector3D v =
OFMakeVector3D(e.x, e.y, (float)S(e.x, e.y)->floor);
for (DynamicEntity *monster in monsters) {
for (Monster *monster in monsters) {
if (monster.state == CS_DEAD) {
if (lastmillis - monster.lastAction < 2000) {
monster.move = 0;
@ -412,17 +478,17 @@ monsterthink()
}
}];
for (DynamicEntity *monster in monsters)
for (Monster *monster in monsters)
if (monster.state == CS_ALIVE)
monsteraction(monster);
[monster performAction];
}
void
monsterrender()
+ (void)renderAll
{
for (DynamicEntity *monster in monsters)
for (Monster *monster in monsters)
renderclient(monster, false,
monstertypes[monster.monsterType].mdlname,
monster.monsterType == 5,
monstertypes[monster.monsterType].mscale / 10.0f);
}
@end

View file

@ -3,6 +3,7 @@
#include "cube.h"
#import "DynamicEntity.h"
#import "Monster.h"
// render players & monsters
// very messy ad-hoc handling of animation frames, should be made more
@ -53,9 +54,11 @@ renderclient(
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;

View file

@ -4,6 +4,7 @@
#import "DynamicEntity.h"
#import "Entity.h"
#import "Monster.h"
#import "OFString+Cube.h"
int nextmode = 0; // nextmode becomes gamemode after next map load
@ -174,7 +175,7 @@ updateworld(int millis) // main game update loop
}
otherplayers();
if (!demoplayback) {
monsterthink();
[Monster thinkAll];
if (player1.state == CS_DEAD) {
if (lastmillis - player1.lastAction < 2000) {
player1.move = player1.strafe = 0;
@ -417,7 +418,7 @@ startmap(OFString *name) // called just after a map load
conoutf(@"coop sp not supported yet");
}
sleepwait = 0;
monsterclear();
[Monster resetAll];
projreset();
spawncycle = -1;
spawnplayer(player1);

View file

@ -126,7 +126,6 @@ enum {
enum { A_BLUE, A_GREEN, A_YELLOW }; // armour types... take 20/40/60 % off
enum {
M_NONE = 0,
M_SEARCH,
M_HOME,
M_ATTACKING,

View file

@ -4,6 +4,7 @@
#include "cube.h"
#import "DynamicEntity.h"
#import "Monster.h"
#import "OFString+Cube.h"
bool editmode = false;
@ -68,8 +69,8 @@ toggleedit()
// 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;

View file

@ -13,6 +13,7 @@ executable('client',
'MapModelInfo.m',
'Menu.m',
'MenuItem.m',
'Monster.m',
'OFString+Cube.m',
'Projectile.m',
'ResolverResult.m',
@ -30,7 +31,6 @@ executable('client',
'entities.m',
'init.m',
'menus.m',
'monster.m',
'physics.m',
'rendercubes.m',
'renderextras.m',

View file

@ -9,6 +9,7 @@
#import "DynamicEntity.h"
#import "Entity.h"
#import "MapModelInfo.h"
#import "Monster.h"
// collide with player or monster
static bool
@ -29,9 +30,9 @@ plcollide(
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;
@ -105,9 +106,10 @@ collide(DynamicEntity *d, bool spawn, float drop, float rise)
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)
float minfloor =
([d isKindOfClass:Monster.class] && !spawn && d.health > 100
? d.origin.z - d.eyeHeight - 4.5f
: -1000.0f;
: -1000.0f);
for (int x = x1; x <= x2; x++) {
for (int y = y1; y <= y2; y++) {
@ -183,7 +185,7 @@ collide(DynamicEntity *d, bool spawn, float drop, float rise)
// 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;
@ -323,7 +325,7 @@ moveplayer4(DynamicEntity *pl, int moveres, bool local, int curtime)
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);
}
@ -332,7 +334,7 @@ moveplayer4(DynamicEntity *pl, int moveres, bool local, int curtime)
// 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);
}

View file

@ -250,15 +250,6 @@ 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<DynamicEntity *> *getmonsters();
extern void monsterpain(DynamicEntity *m, int damage, DynamicEntity *d);
extern void endsp(bool allkilled);
// entities
extern void initEntities();
extern void renderents();

View file

@ -3,6 +3,7 @@
#include "cube.h"
#import "DynamicEntity.h"
#import "Monster.h"
#import "OFString+Cube.h"
#ifdef DARWIN
@ -478,7 +479,7 @@ gl_drawframe(int w, int h, float curfps)
xtraverts = 0;
renderclients();
monsterrender();
[Monster renderAll];
renderentities();

View file

@ -5,6 +5,7 @@
#import "DynamicEntity.h"
#import "Entity.h"
#import "Monster.h"
#ifdef OF_BIG_ENDIAN
static const int islittleendian = 0;
@ -114,9 +115,9 @@ savestate(OFIRI *IRI)
for (Entity *e in ents)
gzputc(f, e.spawned);
gzwrite(f, data.items, data.count);
OFArray<DynamicEntity *> *monsters = getmonsters();
OFArray<Monster *> *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);
}
@ -227,11 +228,11 @@ loadgamerest()
player1.lastAction = lastmillis;
int nmonsters = gzgeti();
OFArray<DynamicEntity *> *monsters = getmonsters();
OFArray<Monster *> *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
@ -241,7 +242,7 @@ loadgamerest()
if (monster.state == CS_DEAD)
monster.lastAction = 0;
}
restoremonsterstate();
[Monster restoreAll];
int nplayers = gzgeti();
loopi(nplayers) if (!gzget())

View file

@ -3,6 +3,7 @@
#include "cube.h"
#import "DynamicEntity.h"
#import "Monster.h"
#import "OFString+Cube.h"
#import "Projectile.h"
@ -168,13 +169,13 @@ newprojectile(const OFVector3D *from, const OFVector3D *to, float speed,
}
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);
@ -235,8 +236,8 @@ splash(Projectile *p, const OFVector3D *v, const OFVector3D *vold,
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);
}];
@ -267,7 +268,7 @@ moveprojectiles(float time)
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;
@ -283,7 +284,7 @@ moveprojectiles(float time)
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);
@ -335,7 +336,7 @@ shootv(int gun, const OFVector3D *from, const OFVector3D *to, DynamicEntity *d,
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;
@ -366,7 +367,7 @@ raydamage(DynamicEntity *o, const OFVector3D *from, const OFVector3D *to,
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;
@ -419,7 +420,7 @@ shoot(DynamicEntity *d, const OFVector3D *targ)
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));
@ -433,10 +434,10 @@ shoot(DynamicEntity *d, const OFVector3D *targ)
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);
}

View file

@ -4,6 +4,7 @@
#import "DynamicEntity.h"
#import "Entity.h"
#import "Monster.h"
extern OFString *entnames[]; // lookup from map entities above to strings
@ -79,7 +80,7 @@ trigger(int tag, int type, bool savegame)
execute(aliasname, true);
if (type == 2)
endsp(false);
[Monster endSinglePlayerWithAllKilled:false];
}
COMMAND(trigger, ARG_2INT)

View file

@ -4,6 +4,7 @@
#import "DynamicEntity.h"
#import "Entity.h"
#import "Monster.h"
extern bool hasoverbright;
@ -205,7 +206,7 @@ dodynlight(const OFVector3D *vold, const OFVector3D *v, int reach, int strength,
{
if (!reach)
reach = dynlight;
if (owner.monsterState)
if ([owner isKindOfClass:Monster.class])
reach = reach / 2;
if (!reach)
return;