816 lines
24 KiB
Plaintext
816 lines
24 KiB
Plaintext
// ----------------------------------------------------------------------
|
|
// Cursed Cacodemon Plushie
|
|
// ----------------------------------------------------------------------
|
|
//
|
|
// Scrawled on the tag is the text "Even in death, I will protect you,
|
|
// and not even the gods can stop me."
|
|
//
|
|
|
|
const HDLD_KIRI_CACOPLUSHIE = "kac";
|
|
const KIRI_CACOPLUSHIE_FAINTED_MONSTER_COOLDOWN = 60;
|
|
const KIRI_CACOPLUSHIE_CLASS = "FlyZapper";
|
|
|
|
// For testing out shield mechanics, if we ever open this up to other
|
|
// monster types...
|
|
//
|
|
// const KIRI_CACOPLUSHIE_CLASS = "Baron";
|
|
//
|
|
// Hey, want to see something totally broken? Spawn an imp healer and
|
|
// raise an army!
|
|
//
|
|
// const KIRI_CACOPLUSHIE_CLASS = "HealerImp";
|
|
|
|
const KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_HEALTH = 1;
|
|
const KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_SHIELD = 2;
|
|
const KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_NAME_INDEX = 3;
|
|
const KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_FAINTED = 4;
|
|
|
|
class KiriCacodemonPlushieProjectile : SlowProjectile
|
|
{
|
|
// Copied some necessary bits from the normal grenade.
|
|
default
|
|
{
|
|
mass 500;
|
|
scale 0.3;
|
|
+bright;
|
|
}
|
|
|
|
// What item threw this?
|
|
actor thrower;
|
|
|
|
states
|
|
{
|
|
spawn:
|
|
APBX BCDE 2 {
|
|
A_Trail();
|
|
} loop;
|
|
}
|
|
|
|
override void ExplodeSlowMissile(line blockingline, actor blockingobject)
|
|
{
|
|
spawnCritter(blockingobject);
|
|
destroy();
|
|
}
|
|
|
|
action void spawnCritter(actor beaned_actor)
|
|
{
|
|
KiriCacodemonPlushie plushie = KiriCacodemonPlushie(invoker.thrower);
|
|
|
|
if(plushie) {
|
|
plushie.spawnCritter(invoker, beaned_actor);
|
|
}
|
|
}
|
|
}
|
|
|
|
class KiriCacodemonPlushie : HDWeapon {
|
|
|
|
static const String KIRI_CACOPLUSHIE_NAMES[] = {
|
|
"<unnamed>",
|
|
"Billy",
|
|
"Jimmy",
|
|
"Bob",
|
|
"Kiddo",
|
|
"Lucky",
|
|
"Daisy",
|
|
"Murderface Skullslasher",
|
|
"Monica, Destroyer of Worlds",
|
|
"Fido",
|
|
"Thor",
|
|
"Loki",
|
|
"Bucky",
|
|
"Taiyo",
|
|
"Fluffy",
|
|
"Leonardo",
|
|
"Donatello",
|
|
"Raphael",
|
|
"Michaelangelo",
|
|
"Winston",
|
|
"Egon",
|
|
"Ray",
|
|
"Peter",
|
|
"Zuul",
|
|
"Gozer",
|
|
"Slimer",
|
|
"Janine",
|
|
"Dana",
|
|
"Quark",
|
|
"Odo",
|
|
"Sisko",
|
|
"Jake",
|
|
"Nog",
|
|
"Rom",
|
|
"Kira",
|
|
"Jadzia",
|
|
"Ezri",
|
|
"Kor",
|
|
"Koloth",
|
|
"Kang",
|
|
"Garrack",
|
|
"Bashir",
|
|
"O'brien",
|
|
"Taco Works"
|
|
};
|
|
|
|
actor spawned_creature;
|
|
actor spawned_spawnball;
|
|
int fainted_monster_despawn_countdown;
|
|
|
|
default
|
|
{
|
|
+hdweapon.fitsinbackpack;
|
|
+weapon.wimpy_weapon;
|
|
|
|
+INVENTORY.PERSISTENTPOWER;
|
|
+INVENTORY.INVBAR;
|
|
|
|
inventory.icon "KCPLD0";
|
|
inventory.pickupsound "misc/p_pkup";
|
|
inventory.pickupmessage "Picked up a cursed Cacodemon plushie.";
|
|
inventory.amount 1;
|
|
inventory.maxamount 1;
|
|
|
|
hdweapon.refid HDLD_KIRI_CACOPLUSHIE;
|
|
|
|
scale 0.63;
|
|
|
|
tag "Cacodemon Plushie";
|
|
}
|
|
|
|
states
|
|
{
|
|
spawn:
|
|
KCPL D 0 -1;
|
|
stop;
|
|
|
|
unload:
|
|
goto ready;
|
|
|
|
firemode:
|
|
goto ready;
|
|
|
|
altfire:
|
|
goto ready;
|
|
|
|
ready:
|
|
KCPL B 0 { A_WeaponReady(WRF_ALL); A_WeaponBusy(false); }
|
|
|
|
// Jump to idle and skip glowing anim if we have a monster or
|
|
// spawnball out.
|
|
KCPL B 0 A_JumpIf(!(invoker.spawned_spawnball || invoker.spawned_creature), 4);
|
|
|
|
// Glowing animation.
|
|
KCPL E 3;
|
|
KCPL F 3;
|
|
KCPL G 3;
|
|
KCPL G 0 A_Jump(255, 2); // Skip idle animation.
|
|
|
|
// Idle animation.
|
|
KCPL B 1;
|
|
KCPL B 0;
|
|
|
|
goto readyend;
|
|
|
|
waiting:
|
|
KCPL B 5;
|
|
KCPL B 0 A_Refire("waiting");
|
|
goto ready;
|
|
|
|
fire:
|
|
KCPL B 2 offset(0, 20);
|
|
KCPL B 2 offset(0, 40);
|
|
KCPL B 2 offset(0, 55) {
|
|
A_StartSound("kiri/cacoplushie_throw");
|
|
}
|
|
|
|
KCPL B 2 offset(0, 70);
|
|
KCPL B 0 {
|
|
if(!invoker.spawned_spawnball && !invoker.spawned_creature) {
|
|
|
|
// No spawn ball or monster. Throw a new one.
|
|
|
|
A_AlertMonsters();
|
|
|
|
bool success;
|
|
actor newcaconade;
|
|
|
|
[success, newcaconade] = A_SpawnItemEx(
|
|
"KiriCacodemonPlushieProjectile",
|
|
xofs : cos(invoker.owner.pitch) * -4,
|
|
yofs : 3, // Offset a little bit for a right handed throw.
|
|
zofs : invoker.owner.height * 0.88 - sin(invoker.owner.pitch) * -4,
|
|
xvel : cos(invoker.owner.pitch) * 20,
|
|
yvel : 0,
|
|
zvel : -sin(invoker.owner.pitch) * 20,
|
|
flags : SXF_NOCHECKPOSITION | SXF_TRANSFERPITCH);
|
|
|
|
if(success) {
|
|
|
|
KiriCacodemonPlushieProjectile newcaconade2 =
|
|
KiriCacodemonPlushieProjectile(newcaconade);
|
|
newcaconade2.thrower = invoker;
|
|
invoker.spawned_spawnball = newcaconade2;
|
|
invoker.fainted_monster_despawn_countdown =
|
|
KIRI_CACOPLUSHIE_FAINTED_MONSTER_COOLDOWN;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Monster or spawn ball is already out. Recall it.
|
|
invoker.despawnMonster();
|
|
}
|
|
|
|
// Reset help text, now that the context has changed.
|
|
invoker.A_SetHelpText();
|
|
}
|
|
KCPL B 2 offset(0, 70);
|
|
KCPL B 2 offset(0, 55);
|
|
KCPL B 2 offset(0, 40);
|
|
KCPL B 2 offset(0, 20);
|
|
goto waiting;
|
|
|
|
reload:
|
|
KCPL B 2 offset(0, 20);
|
|
KCPL B 2 offset(0, 40);
|
|
KCPL B 2 offset(0, 55);
|
|
KCPL B 5 offset(0, 70);
|
|
KCPL B 0 { drinkBloodPack(); }
|
|
KCPL B 2 offset(0, 70);
|
|
KCPL B 2 offset(0, 55);
|
|
KCPL B 2 offset(0, 40);
|
|
KCPL B 2 offset(0, 20);
|
|
goto ready;
|
|
|
|
select0:
|
|
KCPL B 0 offset(0, 120);
|
|
---- B 1 A_Raise(12);
|
|
wait;
|
|
|
|
deselect0:
|
|
KCPL B 0;
|
|
---- B 1 A_Lower(12);
|
|
wait;
|
|
|
|
}
|
|
|
|
action void drinkBloodPack()
|
|
{
|
|
// Refuse to feed active monsters.
|
|
if(invoker.spawned_creature || invoker.spawned_spawnball) {
|
|
if(invoker.owner) {
|
|
invoker.owner.A_Log(
|
|
String.format(
|
|
"Cannot feed blood to %s when they're not in their plushie!",
|
|
invoker.getMonsterName()),
|
|
true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Refuse to feed monsters that don't need any more.
|
|
if(invoker.getMonsterHealth() == invoker.getMonsterMaxHealth()) {
|
|
if(invoker.owner) {
|
|
invoker.owner.A_Log(
|
|
String.format(
|
|
"%s doesn't need any more food for now.",
|
|
invoker.getMonsterName()),
|
|
true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if(invoker.owner.countInv("SecondBlood")) {
|
|
|
|
// Consume a bloodbag and add health.
|
|
A_StartSound("potion/chug");
|
|
invoker.owner.A_TakeInventory("SecondBlood", 1, TIF_NOTAKEINFINITE);
|
|
invoker.setMonsterHealth(
|
|
int(invoker.getMonsterHealth() +
|
|
invoker.getMonsterMaxHealth() * 0.25));
|
|
|
|
// Throw out a spend blood bag.
|
|
A_SpawnItemEx(
|
|
"BloodBagWorn",
|
|
xofs : 0,
|
|
yofs : 0,
|
|
zofs : invoker.owner.height / 2.0,
|
|
xvel : frandom(0, 10),
|
|
yvel : frandom(-5, 5),
|
|
flags : SXF_NOCHECKPOSITION);
|
|
|
|
} else {
|
|
|
|
// No bloodbags available. Take health instead.
|
|
HDBleedingWound hbl = HDBleedingWound.inflict(
|
|
invoker.owner, 2, 1, source : invoker.owner);
|
|
|
|
// FIXME this gets rounded down to 0
|
|
//HDPlayerPawn(invoker.owner).bloodloss += 0.1;
|
|
|
|
A_StartSound(invoker.owner.painsound);
|
|
|
|
invoker.setMonsterHealth(
|
|
int(invoker.getMonsterHealth() +
|
|
invoker.getMonsterMaxHealth() * 0.1));
|
|
}
|
|
}
|
|
|
|
// These are for allowing multiple copies of this weapon at the
|
|
// same time, but the only things on it that get stored are the
|
|
// weaponstatus, meaning that we lose the pointer to the spawned
|
|
// actor. So we have to despawn any actor we have out before
|
|
// switching.
|
|
//
|
|
// Yes, this means you only get one monster to play with at a
|
|
// time.
|
|
override bool AddSpareWeapon(
|
|
actor newowner)
|
|
{
|
|
return AddSpareWeaponRegular(newowner);
|
|
}
|
|
|
|
override hdweapon GetSpareWeapon(actor newowner, bool reverse, bool doselect)
|
|
{
|
|
despawnMonster();
|
|
return GetSpareWeaponRegular(newowner, reverse, doselect);
|
|
}
|
|
|
|
override void beginPlay()
|
|
{
|
|
super.beginPlay();
|
|
setRandomMonsterName();
|
|
setMonsterHealth(getMonsterMaxHealth());
|
|
setMonsterShield(getMonsterMaxShield());
|
|
}
|
|
|
|
int getMonsterHealth() const
|
|
{
|
|
return weaponstatus[KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_HEALTH];
|
|
}
|
|
|
|
int getMonsterMaxHealth() const
|
|
{
|
|
return getDefaultByType(KIRI_CACOPLUSHIE_CLASS).health;
|
|
}
|
|
|
|
void setMonsterHealth(int health)
|
|
{
|
|
if(health > getMonsterMaxHealth()) {
|
|
health = getMonsterMaxHealth();
|
|
}
|
|
weaponstatus[KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_HEALTH] = health;
|
|
|
|
if(health > getMonsterMaxHealth() / 2.0) {
|
|
setMonsterFainted(false);
|
|
}
|
|
}
|
|
|
|
int getMonsterShield() const
|
|
{
|
|
return weaponstatus[KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_SHIELD];
|
|
}
|
|
|
|
int getMonsterMaxShield() const
|
|
{
|
|
return getDefaultByType(KIRI_CACOPLUSHIE_CLASS).maxshields;
|
|
}
|
|
|
|
void setMonsterShield(int shield)
|
|
{
|
|
if(shield > getMonsterMaxShield()) {
|
|
shield = getMonsterMaxShield();
|
|
}
|
|
|
|
weaponstatus[KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_SHIELD] = shield;
|
|
}
|
|
|
|
override void DrawHUDStuff(HDStatusBar sb, HDWeapon hdw, HDPlayerPawn hpl)
|
|
{
|
|
KiriCacodemonPlushie plushie = KiriCacodemonPlushie(hdw);
|
|
String monsterName = getMonsterName();
|
|
int monsterHealth = getMonsterHealth();
|
|
int monsterShield = getMonsterShield();
|
|
int monsterFainted = getMonsterFainted();
|
|
|
|
if(plushie) {
|
|
|
|
// Draw the monster name. Use a green color if it's okay
|
|
// or a red color if it's fainted.
|
|
sb.drawstring(
|
|
sb.psmallfont,
|
|
monsterName, (-20,-30),
|
|
sb.DI_TEXT_ALIGN_RIGHT | sb.DI_TRANSLATABLE | sb.DI_SCREEN_CENTER_BOTTOM,
|
|
monsterFainted ? Font.CR_DARKRED : Font.CR_GREEN);
|
|
|
|
// Draw blood packs where we'd normally put ammo.
|
|
sb.drawimage(
|
|
"PBLDA0",
|
|
(-48,-10),
|
|
sb.DI_SCREEN_CENTER_BOTTOM,
|
|
scale : (0.75, 0.75));
|
|
|
|
sb.drawnum(
|
|
hpl.countinv("SecondBlood"),
|
|
-48, -8,
|
|
sb.DI_SCREEN_CENTER_BOTTOM);
|
|
|
|
// TODO: Draw something like blue potions or batteries for
|
|
// shields?
|
|
|
|
// Draw health.
|
|
sb.drawwepnum(
|
|
monsterHealth,
|
|
getMonsterMaxHealth());
|
|
|
|
// Draw shield.
|
|
sb.drawwepnum(
|
|
monsterShield,
|
|
getMonsterMaxShield(), posy: -10);
|
|
}
|
|
}
|
|
|
|
override double weaponbulk()
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
void setRandomMonsterName()
|
|
{
|
|
weaponstatus[KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_NAME_INDEX] =
|
|
random(1, KiriCacodemonPlushie.KIRI_CACOPLUSHIE_NAMES.size() - 1);
|
|
|
|
A_SetHelpText();
|
|
}
|
|
|
|
String getMonsterName() const
|
|
{
|
|
return KiriCacodemonPlushie.KIRI_CACOPLUSHIE_NAMES[
|
|
weaponstatus[KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_NAME_INDEX]];
|
|
}
|
|
|
|
void despawnMonster()
|
|
{
|
|
// Whatever we despawn, play the despawn sound for it.
|
|
if(spawned_spawnball || spawned_creature) {
|
|
A_StartSound("kiri/cacoplushie_despawn");
|
|
}
|
|
|
|
// If we have a spawn projectile flying through the air,
|
|
// despawn that.
|
|
if(spawned_spawnball) {
|
|
spawned_spawnball.A_SpawnItemEx("TeleFog");
|
|
spawned_spawnball.destroy();
|
|
}
|
|
|
|
// If we have the creature in the world, despawn that.
|
|
if(spawned_creature) {
|
|
spawned_creature.A_SpawnItemEx("TeleFog");
|
|
spawned_creature.destroy();
|
|
|
|
if(owner) {
|
|
owner.A_Log(
|
|
String.format("%s has returned to the plushie.", getMonsterName()),
|
|
true);
|
|
}
|
|
}
|
|
|
|
// Adjust help text because the context changed. Now we can
|
|
// feed it and possibly deploy it.
|
|
A_SetHelpText();
|
|
}
|
|
|
|
void setMonsterFainted(bool fainted)
|
|
{
|
|
weaponstatus[KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_FAINTED] = fainted;
|
|
}
|
|
|
|
bool getMonsterFainted() const
|
|
{
|
|
return weaponstatus[KIRI_CACOPLUSHIE_WEAPONSTATUS_MONSTER_FAINTED];
|
|
}
|
|
|
|
override void DoEffect()
|
|
{
|
|
if(spawned_creature) {
|
|
|
|
// Check on shields.
|
|
HDMobBase mob = HDMobBase(spawned_creature);
|
|
if(mob) {
|
|
Actor shieldItemActor = mob.FindInventory("HDMagicShield");
|
|
HDMagicShield shieldItem = HDMagicShield(shieldItemActor);
|
|
if(shieldItem) {
|
|
|
|
// Counteract natural shield regeneration. We want
|
|
// to limit this to when the monster is asleep.
|
|
if(shieldItem.amount > getMonsterShield()) {
|
|
shieldItem.amount = getMonsterShield();
|
|
}
|
|
|
|
// Notify us if shields have dropped below what we
|
|
// last saw them at.
|
|
if(shieldItem.amount != getMonsterShield()) {
|
|
setMonsterShield(shieldItem.amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Counteract monster health regeneration. We want to
|
|
// limit this to when the player feeds the monster.
|
|
if(spawned_creature.health > getMonsterHealth())
|
|
{
|
|
spawned_creature.health = getMonsterHealth();
|
|
}
|
|
|
|
// Notify us if the health has dropped.
|
|
if(spawned_creature.health != getMonsterHealth())
|
|
{
|
|
setMonsterHealth(spawned_creature.health);
|
|
}
|
|
|
|
// If the monster reaches zero health, then despawn it
|
|
// after a short delay. We still want the death animation
|
|
// to play out.
|
|
if(spawned_creature.health <= 0.0) {
|
|
|
|
setMonsterFainted(true);
|
|
|
|
// On the first frame of the monster being downed,
|
|
// show the message.
|
|
if(fainted_monster_despawn_countdown ==
|
|
KIRI_CACOPLUSHIE_FAINTED_MONSTER_COOLDOWN)
|
|
{
|
|
if(owner) {
|
|
owner.A_Log(String.format("%s has fainted!", getMonsterName()), true);
|
|
}
|
|
}
|
|
|
|
// After some number of frames, actually despawn the
|
|
// monster.
|
|
fainted_monster_despawn_countdown -= 1;
|
|
if(fainted_monster_despawn_countdown <= 0) {
|
|
despawnMonster();
|
|
}
|
|
}
|
|
|
|
// Spawn green balls.
|
|
if(spawned_creature) {
|
|
if(level.time & 1) {
|
|
double mrad = spawned_creature.radius * 0.3;
|
|
spawned_creature.A_SpawnParticle(
|
|
"green", SPF_FULLBRIGHT, 50,
|
|
frandom(4, 8), 0,
|
|
frandom(-mrad, mrad), frandom(-mrad, mrad), frandom(0.1, 0.9) * spawned_creature.height,
|
|
frandom(-0.2, 0.2), frandom(-0.2, 0.2), frandom(0.05, 0.2),
|
|
frandom(-0.05, 0.05), frandom(-0.05, 0.05), 0.06,
|
|
startalphaf : 0.8,
|
|
sizestep : 0.1);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
// Monster is inside plushie. Sloooooooooooooowly
|
|
// regenerate shields, if the monster is awake.
|
|
if(!getMonsterFainted()) {
|
|
|
|
if(getMonsterShield() == -64) {
|
|
|
|
// Shields were totally broken. Extremely low chance
|
|
// to restore shields each frame.
|
|
if(frandom(0, 100) < 0.1) {
|
|
if(owner) {
|
|
owner.A_Log(
|
|
String.format(
|
|
"%s's shields have started charging.",
|
|
getMonsterName()),
|
|
true);
|
|
}
|
|
setMonsterShield(0);
|
|
}
|
|
|
|
} else {
|
|
|
|
// Shields are up and slowly recharging.
|
|
if(frandom(0, 100) < 5) {
|
|
setMonsterShield(getMonsterShield() + 1);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override void consolidate()
|
|
{
|
|
// Restore shields on level exit.
|
|
if(!getMonsterFainted()) {
|
|
setMonsterShield(getMonsterMaxShield());
|
|
}
|
|
|
|
A_SetHelpText();
|
|
}
|
|
|
|
action void spawnCritter(actor spawn_ball, actor beaned_actor)
|
|
{
|
|
bool success = false;
|
|
|
|
// Handle monsters that have already fainted. They won't
|
|
// spawn.
|
|
if(invoker.getMonsterFainted())
|
|
{
|
|
if(invoker.owner) {
|
|
invoker.owner.A_Log(
|
|
String.format(
|
|
"%s has fainted and cannot manifest yet.",
|
|
invoker.getMonsterName()),
|
|
true);
|
|
}
|
|
|
|
spawn_ball.A_SpawnItemEx("TeleFog");
|
|
|
|
return;
|
|
}
|
|
|
|
int tries = 0;
|
|
|
|
// Offset to spawn at. Absolute directions, not relative.
|
|
float offset_x = 0;
|
|
float offset_y = 0;
|
|
float offset_z = 0;
|
|
|
|
// If we hit an enemy in the face, back off of that enemy by
|
|
// the sum of us and their radius. We already know we can't
|
|
// spawn inside of it.
|
|
if(beaned_actor) {
|
|
offset_x -= cos(spawn_ball.angle) * (beaned_actor.radius + 30);
|
|
offset_y -= sin(spawn_ball.angle) * (beaned_actor.radius + 30);
|
|
}
|
|
|
|
int extra_flags = 0; // FIXME: Remove this.
|
|
actor spawned_thing;
|
|
|
|
// Cumulative random offset we'll use to jitter the location a
|
|
// bit in the hopes of finding a valid spot.
|
|
float random_offset_x = 0;
|
|
float random_offset_y = 0;
|
|
|
|
// We need to give this a LOT of leeway as far as where we can
|
|
// spawn. The monsters are quite large compared to the
|
|
// projectile, meaning it can easily get into places that the
|
|
// monsters can't spawn into, so we need to aggressively
|
|
// search for a nearby spot to spawn into.
|
|
while(tries < 30 && !success) {
|
|
|
|
// This will add some jitter that will thrash
|
|
// back-and-forth on both X and Y axes, allowing us to
|
|
// gradually cover a bigger and bigger potential area.
|
|
float jitter_amount = 1;
|
|
if(frandom(0, 1) <= 0.5) {
|
|
random_offset_x = -abs(random_offset_x) - frandom(0, jitter_amount);
|
|
} else {
|
|
random_offset_x = abs(random_offset_x) + frandom(0, jitter_amount);
|
|
}
|
|
if(frandom(0, 1) <= 0.5) {
|
|
random_offset_y = -abs(random_offset_y) - frandom(0, jitter_amount);
|
|
} else {
|
|
random_offset_y = abs(random_offset_y) + frandom(0, jitter_amount);
|
|
}
|
|
|
|
// This will move the offset back a bit, away from the
|
|
// point of contact, and back towards the player, while
|
|
// also mixing in the jitter we calculated.
|
|
offset_x -= (spawn_ball.vel.x * 0.3) + random_offset_x;
|
|
offset_y -= (spawn_ball.vel.y * 0.3) + random_offset_y;
|
|
|
|
// Z axis is a little different. We already know the
|
|
// optimal height to spawn at would be the center of the
|
|
// sector, vertically, so nudge us, on that axis, towards
|
|
// the center of the sector. No need for jitter.
|
|
float sector_center_z =
|
|
(spawn_ball.ceilingz + spawn_ball.floorz) / 2.0;
|
|
if(spawn_ball.pos.z + offset_z > sector_center_z) {
|
|
offset_z -= 2.0;
|
|
} else if(spawn_ball.pos.z + offset_z < sector_center_z) {
|
|
offset_z += 2.0;
|
|
}
|
|
|
|
tries += 1;
|
|
|
|
// Spawn debug particles showing all the points we've
|
|
// tried.
|
|
if(hd_debug > 1) {
|
|
spawn_ball.A_SpawnParticle(
|
|
"yellow",
|
|
flags : 0,
|
|
size : 12,
|
|
xoff : offset_x,
|
|
yoff : offset_y,
|
|
zoff : offset_z);
|
|
}
|
|
|
|
// Attempt to spawn the monster.
|
|
[success, spawned_thing] = spawn_ball.A_SpawnItemEx(
|
|
KIRI_CACOPLUSHIE_CLASS,
|
|
xofs : offset_x,
|
|
yofs : offset_y,
|
|
zofs : offset_z,
|
|
xvel : 0,
|
|
yvel : 0,
|
|
zvel : 0,
|
|
angle : 0,
|
|
flags : SXF_SETMASTER | SXF_ABSOLUTEPOSITION | extra_flags);
|
|
|
|
if(success) {
|
|
|
|
// Spawn succeeded. Stop searching.
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(success) {
|
|
|
|
// Copy health over.
|
|
spawned_thing.health = invoker.getMonsterHealth();
|
|
|
|
// Copy shields over (if applicable).
|
|
Actor shieldItemActor = spawned_thing.FindInventory("HDMagicShield");
|
|
HDMagicShield shieldItem = HDMagicShield(shieldItemActor);
|
|
if(shieldItem) {
|
|
shieldItem.amount = invoker.getMonsterShield();
|
|
}
|
|
|
|
// Set the name.
|
|
spawned_thing.setTag(invoker.getMonsterName());
|
|
|
|
// FIXME: Would prefer something to make the monster more
|
|
// distinct. Other attempts were made to distinguish the
|
|
// monster...
|
|
//
|
|
// This one was good because it made the monsters very
|
|
// distinct (built-in GZDoom translation):
|
|
//
|
|
// spawned_thing.settranslation("Ice")
|
|
// spawned_thing.translation = TRANSLATION_ICE;
|
|
//
|
|
// This would make it render in the transparent style of
|
|
// the ghosts (minus the coloration):
|
|
//
|
|
// spawned_thing.A_SetRenderStyle(1.0, STYLE_ADD);
|
|
//
|
|
// This one just made it all purple. Certainly looks
|
|
// distinct!
|
|
//
|
|
// spawned_thing.A_SetTranslation("AllPurple");
|
|
//
|
|
// This would just copy the player colors over.
|
|
//
|
|
// spawned_thing.translation = invoker.owner.translation;
|
|
|
|
// Spawn flash.
|
|
spawn_ball.A_SpawnItemEx(
|
|
"TeleFog",
|
|
xofs : offset_x,
|
|
yofs : offset_y,
|
|
zofs : offset_z,
|
|
flags : SXF_ABSOLUTEPOSITION);
|
|
|
|
// Keep track of the spawned creature.
|
|
invoker.spawned_creature = spawned_thing;
|
|
|
|
// If we smacked a mob in the face, then make that the
|
|
// spawned monster's target immediately.
|
|
if(beaned_actor) {
|
|
if(beaned_actor is "HDMobBase") {
|
|
spawned_thing.target = beaned_actor;
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
// Tell the player we failed.
|
|
if(invoker.owner) {
|
|
invoker.owner.A_Log(
|
|
String.format("Failed to summon %s: Not enough room.", invoker.getMonsterName()),
|
|
true);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
override String, double getPickupSprite(bool usespare)
|
|
{
|
|
return "KCPLD0";
|
|
}
|
|
|
|
override String getHelpText()
|
|
{
|
|
if(spawned_creature || spawned_spawnball) {
|
|
return WEPHELP_FIRE.." Return "..getMonsterName().." to the plushie.\n";
|
|
}
|
|
|
|
return
|
|
WEPHELP_FIRE .. " Summon "..getMonsterName()..".\n"..
|
|
WEPHELP_RELOAD .. " Feed blood to "..getMonsterName()..".\n";
|
|
}
|
|
}
|