Two points to keep in mind about this tutorial:
1. It looks stupidly long because I'm documenting every step and re-posting the entire modified code each time. It is intentionally repetitive.
2. I do not intend anyone to use this code as a precedent. There are numerous decisions here that are actually suboptimal and could be changed to make it more efficient or maintainable, but are left this way since it follows the flow of the tutorial itself and I really don't want to get into coding best practices or whatever because that would be the blind person with the poor spatial memory and Dunning-Kruger leading the blind.
Premise: are you tired of having to walk all the way over to a zombieman's corpse to pick up his clip every time you kill one? With this mod you can double your efficiency having the clips spawn only half as far away!
First, create a plaintext file called "zscript.txt" and copypaste the following:
Code: Select all
version "3.3" // you must enter this (or the appropriate number)
// or you will be limited to features from 2.3.0
class LazyZom:Zombieman replaces Zombieman{}
Now when you run it it should make no difference whatsoever (except breaking a few dynamic light definitions). Not very exciting! So let's start working on this for real...
The first real change we need is to get rid of the default dropitem so at the end of the day we should only have the one that drops at the closer distance. Unlike DECORATE, all flags and properties must be under a "default" section, and properties (not flags!) must end in a semicolon:
Code: Select all
version "3.3"
class LazyZom:Zombieman replaces Zombieman{
default{
dropitem "none";
}
}
Code: Select all
version "3.3"
class LazyZom:Zombieman replaces Zombieman{
default{
dropitem "none";
}
states{
death:
---- A 0 A_LazyDrop("Clip");
goto super::death;
xdeath:
---- A 0 A_LazyDrop("Clip");
goto super::xdeath;
}
}
Note the different syntax: All state lines must end in semicolons and all function calls must end with () even if they don't normally take any parameters.
Now obviously we haven't actually defined A_LazyDrop so the above won't run. Just to make sure it runs at all and we don't have any stray brackets, let's just put in an empty function:
Code: Select all
version "3.3"
class LazyZom:Zombieman replaces Zombieman{
default{
dropitem "none";
}
states{
death:
---- A 0 A_LazyDrop("Clip");
goto super::death;
xdeath:
---- A 0 A_LazyDrop("Clip");
goto super::xdeath;
}
void A_LazyDrop(class<Actor> todrop = "Clip"){}
}
At this point you should just get a normal zombieman that drops nothing. Now for the fun part - making it drop the thing!
Code: Select all
version "3.3"
class LazyZom:Zombieman replaces Zombieman{
default{
dropitem "none";
}
states{
death:
---- A 0 A_LazyDrop("Clip");
goto super::death;
xdeath:
---- A 0 A_LazyDrop("Clip");
goto super::xdeath;
}
void A_LazyDrop(class<actor> todrop = "Clip"){
Spawn(todrop, self.pos, ALLOW_REPLACE);
}
}
So let's at least mark it as dropped and give it something that kinda looks like the drop movement:
Code: Select all
version "3.3"
class LazyZom:Zombieman replaces Zombieman{
default{
dropitem "none";
}
states{
death:
---- A 0 A_LazyDrop("Clip");
goto super::death;
xdeath:
---- A 0 A_LazyDrop("Clip");
goto super::xdeath;
}
void A_LazyDrop(class<actor> todrop = "Clip"){
actor a = Spawn(todrop, self.pos, ALLOW_REPLACE);
a.bdropped = true;
a.vel.x = random(-1,1);
a.vel.y = random(-1,1);
a.vel.z = 8;
}
}
Note how all this stuff - velocity, flags, etc. - needs to be defined explicitly, unlike A_SpawnItemEx. This might seem like more work, but once you're used to it it sure beats trying to remember all 29+ A_SpawnItemEx flags, the assumptions they're intended to override and what side effects each one has!
[2017-12-28 performance/optimization note: For more advanced or complex uses you might actually want to go back to A_SpawnItemEx, since every step you're putting through the ZScript virtual machine makes your code run that much slower. Sometimes I find it useful to use the approach in this tutorial to begin with and then change it to a Decorate command that relies on native functions that don't put as much load on the VM.]
Now getting the amount right is a bit trickier. ZScript won't recognize the "amount" property right off the bat since it's not found in all actors, just inventory and ammo, so you need to use the magic word "let" to define a variable that ZScript knows for sure is a kind of ammo and thus will definitely have that property to set:
Code: Select all
version "3.3"
class LazyZom:Zombieman replaces Zombieman{
default{
dropitem "none";
}
states{
death:
---- A 0 A_LazyDrop("Clip");
goto super::death;
xdeath:
---- A 0 A_LazyDrop("Clip");
goto super::xdeath;
}
void A_LazyDrop(class<actor> todrop = "Clip"){
actor a = Spawn(todrop, self.pos, ALLOW_REPLACE);
a.bdropped = true;
a.vel.x = random(-1,1);
a.vel.y = random(-1,1);
a.vel.z = 8;
let aa = Ammo(a);
if(aa) aa.amount *= 0.5;
}
}
Code: Select all
version "3.3"
class LazyZom:Zombieman replaces Zombieman{
default{
dropitem "none";
}
states{
death:
---- A 0 A_LazyDrop("Clip");
goto super::death;
xdeath:
---- A 0 A_LazyDrop("Clip");
goto super::xdeath;
}
void A_LazyDrop(class<actor> todrop = "Clip"){
actor a = Spawn(todrop, self.pos, ALLOW_REPLACE);
a.bdropped = true;
a.vel.x = random(-1,1);
a.vel.y = random(-1,1);
a.vel.z = 8;
let aa = Ammo(a);
if(aa) aa.amount *= 0.5;
a.vel += self.vel;
}
}
This is, of course, doing the exact opposite of what we were advertising: go punch a zombie while berserk and the clip will end up even further away than it otherwise would!
So let's make that velocity inheritance contingent... and finally do that halfway spawning thing!:
Code: Select all
version "3.3"
class LazyZom:Zombieman replaces Zombieman{
default{
dropitem "none";
}
states{
death:
---- A 0 A_LazyDrop("Clip");
goto super::death;
xdeath:
---- A 0 A_LazyDrop("Clip");
goto super::xdeath;
}
void A_LazyDrop(class<actor> todrop = "Clip"){
actor a = Spawn(todrop, self.pos, ALLOW_REPLACE);
a.bdropped = true;
a.vel.x = random(-1,1);
a.vel.y = random(-1,1);
a.vel.z = 8;
let aa = Ammo(a);
if(aa) aa.amount *= 0.5;
if(target && target is "PlayerPawn"){
a.SetXYZ( (target.pos + a.pos) * 0.5 );
}else a.vel += self.vel;
}
}
Now if the zombie has a target (gotta check this first to make sure the computer isn't trying to work with a null pointer) and that target is a player, it will set the clip's position to halfway between the zom and the target player instead of adding the zom's velocity.
In other words, if you shoot a zombie at 20 paces, the zombie will drop dead where it is while a clip will pop up out of thin air only 10 paces away! Have fun!
...except there's a bug!
The way it's written now, sometimes the clip will hit a stair and get stuck in midair and unable to be picked up. It seems that calling SetXYZ right in the middle of setting everything up like that might have some side effects involving the engine trying to figure out which sector it's in.
To get around that, we'll just rewrite the conditional so that the initial spawning point is already the point that we want:
Code: Select all
version "3.3"
class LazyZom:Zombieman replaces Zombieman{
default{
dropitem "none";
}
states{
death:
---- A 0 A_LazyDrop("Clip");
goto super::death;
xdeath:
---- A 0 A_LazyDrop("Clip");
goto super::xdeath;
}
void A_LazyDrop(class<actor> todrop = "Clip"){
actor a;
if(target && target is "PlayerPawn"){
a = Spawn(todrop, (target.pos + self.pos) * 0.5, ALLOW_REPLACE);
}else{
a = Spawn(todrop, self.pos, ALLOW_REPLACE);
a.vel += self.vel;
}
a.bdropped = true;
a.vel.x = random(-1,1);
a.vel.y = random(-1,1);
a.vel.z = 8;
let aa = Ammo(a);
if(aa) aa.amount *= 0.5;
}
}
Bonus: You can also give the poor zombieman a parting shot on the same principles:
Code: Select all
version "3.3"
class LazyZom:Zombieman replaces Zombieman{
default{
dropitem "none";
}
states{
death:
---- A 0 A_LazyDrop("Clip");
goto super::death;
xdeath:
---- A 0 A_LazyDrop("Clip");
goto super::xdeath;
}
void A_LazyDrop(class<actor> todrop = "Clip"){
actor a;
if(target && target is "PlayerPawn"){
a = Spawn(todrop, (target.pos + self.pos) * 0.5, ALLOW_REPLACE);
self.bsolid = false; //since this is happening before A_NoBlocking()
actor b = Spawn("RevenantTracer", pos + (0,0,32), ALLOW_REPLACE);
b.tracer = target;
b.target = self;
b.A_FaceTracer(0,0);
b.A_PlaySound("skeleton/attack");
b.vel.x = cos(angle) * cos(pitch) * 10;
b.vel.y = sin(angle) * cos(pitch) * 10;
b.vel.z = -sin(pitch) * 10;
}else{
a = Spawn(todrop, self.pos, ALLOW_REPLACE);
a.vel += self.vel;
}
a.bdropped = true;
a.vel.x += random(-1,1);
a.vel.y += random(-1,1);
a.vel.z += 8;
let aa = Ammo(a);
if(aa) aa.amount *= 0.5;
}
}
And, just for kicks, a rocket that inherits your momentum just as it's finished entering the playsim:
Code: Select all
version "3.3"
class AwkwardRocket:Rocket replaces Rocket{
override void PostBeginPlay(){
super.PostBeginPlay(); //does all the parent actor's initialization stuff
if(target) vel += target.vel;
}
}
And, of course, if you really want to feel the full sense of initiation:
Code: Select all
version "3.3"
class HelloRocket:Rocket replaces Rocket{
override void PostBeginPlay(){
super.PostBeginPlay();
if(target){
vel += target.vel;
if(target is "PlayerPawn") target.A_Log("Hello world!",true);
}
}
}