mirror of
https://github.com/ZDoom/gzdoom.git
synced 2024-11-27 14:22:13 +00:00
ce1aa7e962
With this, one can use its self-replacement code (which copies a bunch of its state into the replacement actor, and monitors for boss death if appropriate), but select the replacement class based on some other criteria (map number, the player's RPG stats, the player's class, etc).
261 lines
6.8 KiB
Text
261 lines
6.8 KiB
Text
|
|
// Random spawner ----------------------------------------------------------
|
|
|
|
class RandomSpawner : Actor
|
|
{
|
|
|
|
const MAX_RANDOMSPAWNERS_RECURSION = 32; // Should be largely more than enough, honestly.
|
|
|
|
Default
|
|
{
|
|
+NOBLOCKMAP
|
|
+NOSECTOR
|
|
+NOGRAVITY
|
|
+THRUACTORS
|
|
}
|
|
|
|
virtual void PostSpawn(Actor spawned)
|
|
{}
|
|
|
|
static bool IsMonster(DropItem di)
|
|
{
|
|
class<Actor> pclass = di.Name;
|
|
if (null == pclass)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return GetDefaultByType(pclass).bIsMonster;
|
|
}
|
|
|
|
// Override this to decide what to spawn in some other way.
|
|
// Return the class name, or 'None' to spawn nothing, or 'Unknown' to spawn an error marker.
|
|
virtual Name ChooseSpawn()
|
|
{
|
|
DropItem di; // di will be our drop item list iterator
|
|
DropItem drop; // while drop stays as the reference point.
|
|
int n = 0;
|
|
bool nomonsters = sv_nomonsters || level.nomonsters;
|
|
|
|
drop = di = GetDropItems();
|
|
if (di != null)
|
|
{
|
|
while (di != null)
|
|
{
|
|
bool shouldSkip = (di.Name == 'None') || (nomonsters && IsMonster(di));
|
|
|
|
if (!shouldSkip)
|
|
{
|
|
int amt = di.Amount;
|
|
if (amt < 0) amt = 1; // default value is -1, we need a positive value.
|
|
n += amt; // this is how we can weight the list.
|
|
}
|
|
|
|
di = di.Next;
|
|
}
|
|
if (n == 0)
|
|
{ // Nothing left to spawn. They must have all been monsters, and monsters are disabled.
|
|
return 'None';
|
|
}
|
|
// Then we reset the iterator to the start position...
|
|
di = drop;
|
|
// Take a random number...
|
|
n = random[randomspawn](0, n-1);
|
|
// And iterate in the array up to the random number chosen.
|
|
while (n > -1 && di != null)
|
|
{
|
|
if (di.Name != 'None' &&
|
|
(!nomonsters || !IsMonster(di)))
|
|
{
|
|
int amt = di.Amount;
|
|
if (amt < 0) amt = 1;
|
|
n -= amt;
|
|
if ((di.Next != null) && (n > -1))
|
|
di = di.Next;
|
|
else
|
|
n = -1;
|
|
}
|
|
else
|
|
{
|
|
di = di.Next;
|
|
}
|
|
}
|
|
if (di == null)
|
|
{
|
|
return 'Unknown';
|
|
}
|
|
else if (random[randomspawn]() <= di.Probability) // prob 255 = always spawn, prob 0 = almost never spawn.
|
|
{
|
|
return di.Name;
|
|
}
|
|
else
|
|
{
|
|
return 'None';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return 'None';
|
|
}
|
|
}
|
|
|
|
// To handle "RandomSpawning" missiles, the code has to be split in two parts.
|
|
// If the following code is not done in BeginPlay, missiles will use the
|
|
// random spawner's velocity (0...) instead of their own.
|
|
override void BeginPlay()
|
|
{
|
|
Super.BeginPlay();
|
|
let s = ChooseSpawn();
|
|
|
|
if (s == 'Unknown') // Spawn error markers immediately.
|
|
{
|
|
Spawn(s, Pos, NO_REPLACE);
|
|
Destroy();
|
|
}
|
|
else if (s == 'None') // ChooseSpawn chose to spawn nothing.
|
|
{
|
|
Destroy();
|
|
}
|
|
else
|
|
{
|
|
// So now we can spawn the dropped item.
|
|
// Handle replacement here so as to get the proper speed and flags for missiles
|
|
Class<Actor> cls = s;
|
|
if (cls != null)
|
|
{
|
|
Class<Actor> rep = GetReplacement(cls);
|
|
if (rep != null)
|
|
{
|
|
cls = rep;
|
|
}
|
|
}
|
|
if (cls != null)
|
|
{
|
|
Species = Name(cls);
|
|
readonly<Actor> defmobj = GetDefaultByType(cls);
|
|
Speed = defmobj.Speed;
|
|
bMissile |= defmobj.bMissile;
|
|
bSeekerMissile |= defmobj.bSeekerMissile;
|
|
bSpectral |= defmobj.bSpectral;
|
|
}
|
|
else
|
|
{
|
|
A_Log(TEXTCOLOR_RED .. "Unknown item class ".. s .." to drop from a random spawner\n");
|
|
Species = 'None';
|
|
}
|
|
}
|
|
}
|
|
|
|
// The second half of random spawning. Now that the spawner is initialized, the
|
|
// real actor can be created. If the following code were in BeginPlay instead,
|
|
// missiles would not have yet obtained certain information that is absolutely
|
|
// necessary to them -- such as their source and destination.
|
|
override void PostBeginPlay()
|
|
{
|
|
Super.PostBeginPlay();
|
|
|
|
if (bouncecount >= MAX_RANDOMSPAWNERS_RECURSION) // Prevents infinite recursions
|
|
{
|
|
Spawn("Unknown", Pos, NO_REPLACE); // Show that there's a problem.
|
|
Destroy();
|
|
return;
|
|
}
|
|
|
|
Actor newmobj = null;
|
|
bool boss = false;
|
|
|
|
if (Species == 'None')
|
|
{
|
|
Destroy();
|
|
return;
|
|
}
|
|
Class<Actor> cls = Species;
|
|
if (bMissile && target && target.target) // Attempting to spawn a missile.
|
|
{
|
|
if ((tracer == null) && bSeekerMissile)
|
|
{
|
|
tracer = target.target;
|
|
}
|
|
newmobj = target.SpawnMissileXYZ(Pos, target.target, cls, false);
|
|
}
|
|
else
|
|
{
|
|
newmobj = Spawn(cls, Pos, NO_REPLACE);
|
|
}
|
|
if (newmobj != null)
|
|
{
|
|
// copy everything relevant
|
|
newmobj.SpawnAngle = SpawnAngle;
|
|
newmobj.Angle = Angle;
|
|
newmobj.Pitch = Pitch;
|
|
newmobj.Roll = Roll;
|
|
newmobj.SpawnPoint = SpawnPoint;
|
|
newmobj.special = special;
|
|
newmobj.args[0] = args[0];
|
|
newmobj.args[1] = args[1];
|
|
newmobj.args[2] = args[2];
|
|
newmobj.args[3] = args[3];
|
|
newmobj.args[4] = args[4];
|
|
newmobj.special1 = special1;
|
|
newmobj.special2 = special2;
|
|
newmobj.SpawnFlags = SpawnFlags & ~MTF_SECRET; // MTF_SECRET needs special treatment to avoid incrementing the secret counter twice. It had already been processed for the spawner itself.
|
|
newmobj.HandleSpawnFlags();
|
|
newmobj.SpawnFlags = SpawnFlags;
|
|
newmobj.bCountSecret = SpawnFlags & MTF_SECRET; // "Transfer" count secret flag to spawned actor
|
|
newmobj.ChangeTid(tid);
|
|
newmobj.Vel = Vel;
|
|
newmobj.master = master; // For things such as DamageMaster/DamageChildren, transfer mastery.
|
|
newmobj.target = target;
|
|
newmobj.tracer = tracer;
|
|
newmobj.CopyFriendliness(self, false);
|
|
// This handles things such as projectiles with the MF4_SPECTRAL flag that have
|
|
// a health set to -2 after spawning, for internal reasons.
|
|
if (health != SpawnHealth()) newmobj.health = health;
|
|
if (!bDropped) newmobj.bDropped = false;
|
|
// Handle special altitude flags
|
|
if (newmobj.bSpawnCeiling)
|
|
{
|
|
newmobj.SetZ(newmobj.ceilingz - newmobj.Height - SpawnPoint.Z);
|
|
}
|
|
else if (newmobj.bSpawnFloat)
|
|
{
|
|
double space = newmobj.ceilingz - newmobj.Height - newmobj.floorz;
|
|
if (space > 48)
|
|
{
|
|
space -= 40;
|
|
newmobj.SetZ((space * random[randomspawn]()) / 256. + newmobj.floorz + 40);
|
|
}
|
|
newmobj.AddZ(SpawnPoint.Z);
|
|
}
|
|
if (newmobj.bMissile)
|
|
newmobj.CheckMissileSpawn(0);
|
|
// Bouncecount is used to count how many recursions we're in.
|
|
if (newmobj is 'RandomSpawner')
|
|
newmobj.bouncecount = ++bouncecount;
|
|
// If the spawned actor has either of those flags, it's a boss.
|
|
if (newmobj.bBossDeath || newmobj.bBoss)
|
|
boss = true;
|
|
// If a replaced actor has either of those same flags, it's also a boss.
|
|
readonly<Actor> rep = GetDefaultByType(GetReplacee(GetClass()));
|
|
if (rep && (rep.bBossDeath || rep.bBoss))
|
|
boss = true;
|
|
|
|
PostSpawn(newmobj);
|
|
}
|
|
if (boss)
|
|
tracer = newmobj;
|
|
else // "else" because a boss-replacing spawner must wait until it can call A_BossDeath.
|
|
Destroy();
|
|
}
|
|
|
|
override void Tick() // This function is needed for handling boss replacers
|
|
{
|
|
Super.Tick();
|
|
if (tracer == null || tracer.health <= 0)
|
|
{
|
|
A_BossDeath();
|
|
Destroy();
|
|
}
|
|
}
|
|
|
|
}
|