diff --git a/sndinfo.txt b/sndinfo.txt index 7fdcf02..5d6837c 100644 --- a/sndinfo.txt +++ b/sndinfo.txt @@ -1,3 +1,5 @@ kiri/gretchencounter_click "sounds/kirigretchencounter_click.ogg" kiri/gretchencounter_blip "sounds/kirigretchencounter_blip.ogg" kiri/gretchencounter_onoff "sounds/kirigretchencounter_onoff.ogg" +kiri/cacoplushie_throw "sounds/kiri_caco_throw.ogg" +kiri/cacoplushie_despawn "sounds/kiri_caco_despawn.ogg" diff --git a/source_data/Makefile b/source_data/Makefile index e5aee8a..f59c22d 100644 --- a/source_data/Makefile +++ b/source_data/Makefile @@ -22,8 +22,8 @@ all : \ ../sprites/cacoplush/cacoplushie_idle.png \ ../sprites/cacoplush/cacoplushie_glowing1.png \ ../sprites/cacoplush/cacoplushie_glowing2.png \ - ../sprites/cacoplush/cacoplushie_glowing3.png - + ../sprites/cacoplush/cacoplushie_glowing3.png \ + ../sprites/cacoplush/cacoplushie_pickup.png # Base frames ../sprites/gretchencounter/kgcma0.png : gretchencounter_weaponsprite.aseprite @@ -366,19 +366,29 @@ brightmaps : povsprite.aseprite # Sounds ../sounds/kiri_caco_despawn.ogg : kiri_caco_despawn.wav ffmpeg -i $^ "-filter:a" "volume=1.0" $@ + ../sounds/kiri_caco_throw.ogg : kiri_caco_throw.wav ffmpeg -i $^ "-filter:a" "volume=1.0" $@ - +# Sprites ../sprites/cacoplush/cacoplushie_idle.png : cacoplushie.aseprite aseprite -b $^ \ --ignore-layer "brightmap" \ + --scale 0.77 \ --frame-range 0,0 --save-as $@ +../sprites/cacoplush/cacoplushie_pickup.png : cacoplushie.aseprite + aseprite -b $^ \ + --ignore-layer "brightmap" \ + --frame-range 0,0 \ + --scale 0.25 \ + --save-as $@ + ../sprites/cacoplush/cacoplushie_glowing1.png \ ../sprites/cacoplush/cacoplushie_glowing2.png \ ../sprites/cacoplush/cacoplushie_glowing3.png : cacoplushie.aseprite aseprite -b $^ \ + --scale 0.77 \ --ignore-layer "brightmap" \ --frame-range 1,3 --save-as ../sprites/cacoplush/cacoplushie_glowing.png diff --git a/sprites/cacoplush/cacoplushie_glowing1.png b/sprites/cacoplush/cacoplushie_glowing1.png index 45be1e4..ea81390 100644 Binary files a/sprites/cacoplush/cacoplushie_glowing1.png and b/sprites/cacoplush/cacoplushie_glowing1.png differ diff --git a/sprites/cacoplush/cacoplushie_glowing2.png b/sprites/cacoplush/cacoplushie_glowing2.png index 6d54e7e..1a62e04 100644 Binary files a/sprites/cacoplush/cacoplushie_glowing2.png and b/sprites/cacoplush/cacoplushie_glowing2.png differ diff --git a/sprites/cacoplush/cacoplushie_glowing3.png b/sprites/cacoplush/cacoplushie_glowing3.png index 96fb5f8..ddd2594 100644 Binary files a/sprites/cacoplush/cacoplushie_glowing3.png and b/sprites/cacoplush/cacoplushie_glowing3.png differ diff --git a/sprites/cacoplush/cacoplushie_idle.png b/sprites/cacoplush/cacoplushie_idle.png index 3e47a70..ab6cd4d 100644 Binary files a/sprites/cacoplush/cacoplushie_idle.png and b/sprites/cacoplush/cacoplushie_idle.png differ diff --git a/sprites/cacoplush/cacoplushie_pickup.png b/sprites/cacoplush/cacoplushie_pickup.png new file mode 100644 index 0000000..09a1f51 Binary files /dev/null and b/sprites/cacoplush/cacoplushie_pickup.png differ diff --git a/textures.txt b/textures.txt index fb70ebc..6ed73f2 100644 --- a/textures.txt +++ b/textures.txt @@ -60,3 +60,39 @@ sprite JMPPC0, 320, 200 { patch JMPPC0,0,0 { } } +// ---------------------------------------------------------------------- +// Caco plushie + +// First-person view while holding. +Sprite "KCPLB0", 100, 83 +{ + Offset -120, -100 + Patch "sprites/cacoplush/cacoplushie_idle.png", 0, 0 +} + +// Pickup sprite on the ground. +Sprite "KCPLD0", 32, 27 +{ + Offset 16, 13 + Patch "sprites/cacoplush/cacoplushie_pickup.png", 0, 0 +} + +// Glowing frames. +Sprite "KCPLE0", 100, 83 +{ + Offset -120, -100 + Patch "sprites/cacoplush/cacoplushie_glowing1.png", 0, 0 +} + +Sprite "KCPLF0", 100, 83 +{ + Offset -120, -100 + Patch "sprites/cacoplush/cacoplushie_glowing2.png", 0, 0 +} + +Sprite "KCPLG0", 100, 83 +{ + Offset -120, -100 + Patch "sprites/cacoplush/cacoplushie_glowing3.png", 0, 0 +} + diff --git a/zscript.zs b/zscript.zs index 1ca7d9e..b1df6b6 100644 --- a/zscript.zs +++ b/zscript.zs @@ -3,3 +3,4 @@ version "4.10" #include "zscript/snektech.zs" #include "zscript/jumpercables.zs" #include "zscript/gretchencounter.zs" +#include "zscript/cacoplushie.zs" diff --git a/zscript/cacoplushie.zs b/zscript/cacoplushie.zs new file mode 100644 index 0000000..853f0ed --- /dev/null +++ b/zscript/cacoplushie.zs @@ -0,0 +1,795 @@ +// ---------------------------------------------------------------------- +// 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 = "Trilobite"; + +// For testing out shield mechanics, if we ever open this up to other +// monster types... +// +// const KIRI_CACOPLUSHIE_CLASS = "PainLord"; +// +// Hey, want to see something totally broken? Spawn an imp healer and +// raise an army! +// +// const KIRI_CACOPLUSHIE_CLASS = "Regentipede"; + +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; + } + + // 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[] = { + "", + "Billy", + "Jimmy", + "Bob", + "Kiddo", + "Lucky", + "Daisy", + "Murderface Skullslasher", + "Monica, Destroyer of Worlds", + "Fido", + "Thor", + "Loki", + "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; + + +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( + 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); + + HDPlayerPawn(invoker.owner).bloodloss += 0.1; + + A_StartSound(invoker.owner.painsound); + + invoker.setMonsterHealth( + 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(); + } + } + + } 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(); + } + + // Copy player colors over. + spawned_thing.translation = invoker.owner.translation; + + // 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"); + + // 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"; + } +}