// ---------------------------------------------------------------------- // 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" }; 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) { 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()) { 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(); 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) { 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) { 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()) { 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. 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"; } }