HDSnekTechDiscountPartyPack/zscript/cacoplushie.zs

816 lines
24 KiB
Plaintext
Raw Normal View History

2023-09-03 10:03:56 -07:00
// ----------------------------------------------------------------------
// 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;
2024-11-09 08:24:46 -08:00
const KIRI_CACOPLUSHIE_CLASS = "FlyZapper";
2023-09-03 10:03:56 -07:00
// For testing out shield mechanics, if we ever open this up to other
// monster types...
//
// const KIRI_CACOPLUSHIE_CLASS = "Baron";
2023-09-03 10:03:56 -07:00
//
// Hey, want to see something totally broken? Spawn an imp healer and
// raise an army!
//
// const KIRI_CACOPLUSHIE_CLASS = "HealerImp";
2023-09-03 10:03:56 -07:00
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;
2023-09-03 16:20:24 -07:00
+bright;
2023-09-03 10:03:56 -07:00
}
// 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",
2023-09-03 10:03:56 -07:00
"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;
2023-09-08 18:32:11 -07:00
+weapon.wimpy_weapon;
2023-09-03 10:03:56 -07:00
+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;
2023-09-03 16:20:24 -07:00
tag "Cacodemon Plushie";
2023-09-03 10:03:56 -07:00
}
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));
2023-09-03 10:03:56 -07:00
// 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;
2023-09-03 10:03:56 -07:00
A_StartSound(invoker.owner.painsound);
invoker.setMonsterHealth(
int(invoker.getMonsterHealth() +
invoker.getMonsterMaxHealth() * 0.1));
2023-09-03 10:03:56 -07:00
}
}
// 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();
}
}
2023-09-03 16:20:24 -07:00
// 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);
}
}
2023-09-03 10:03:56 -07:00
} 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");
2023-09-03 16:20:24 -07:00
//
// This would just copy the player colors over.
//
// spawned_thing.translation = invoker.owner.translation;
2023-09-03 10:03:56 -07:00
// 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";
}
}