// ---------------------------------------------------------------------- // Spraypaint cans // ---------------------------------------------------------------------- // // Super cheap. // const KIRI_SPRAY_DISTANCE = 96; const KIRI_SPRAY_SHAKEANIM_MAXANGLE = 20.0; const KIRI_SPRAY_MAXPAINT = 50; const KIRI_SPRAY_MAXPRESSURE = 100; const KIRI_SPRAY_PRESSUREBUILDSCALE = 20; const KIRI_SPRAY_PRESSURE_PER_USE = (KIRI_SPRAY_MAXPRESSURE / 2); const KIRI_SPRAY_PRESSURE_DECAY_RATE = 0.1; const HDLD_KIRI_SPRAYCAN = "ksp"; enum KiriSprayerStatus { KIRI_SPRAY_WS_PAINT = 1, KIRI_SPRAY_WS_PRESSURE = 2 } class SnekTechSprayer : HDWeapon { default { radius 2; height 4; +SpriteAngle; Scale 0.4; +hdweapon.fitsinbackpack; +weapon.wimpy_weapon; +INVENTORY.PERSISTENTPOWER; +INVENTORY.INVBAR; // inventory.icon "KSPRB0"; inventory.pickupsound "kiri/spraycan_rattle"; inventory.pickupmessage "Picked up some spraypaint cans."; inventory.amount 1; inventory.maxamount 100; hdweapon.refid HDLD_KIRI_SPRAYCAN; tag "Spraypaint Can"; } states { spawn: KSPR B -1; stop; ready: KSPR A 1 { A_WeaponReady(WRF_ALL); A_WeaponBusy(false); } goto readyend; altfire: reload: KSPR A 1 offset(0, 20) { A_OverlayPivot(0, 0.5, 1.0); A_OverlayRotate(0, 0); A_StartSound( "kiri/spraycan_rattle", CHAN_WEAPON, CHANF_NOSTOP); } KSPR A 1 offset(0, 40) { A_OverlayRotate(0, sin(level.time * 5.0) * KIRI_SPRAY_SHAKEANIM_MAXANGLE * 1.0/3.0); } KSPR A 1 offset(0, 60) { A_OverlayRotate(0, sin(level.time * 5.0) * KIRI_SPRAY_SHAKEANIM_MAXANGLE * 2.0/3.0); } KSPR A 1 offset(0, 80) { A_OverlayRotate(0, sin(level.time * 5.0) * KIRI_SPRAY_SHAKEANIM_MAXANGLE); // Taper off pressure increase amount based on how much // pressure is in there already. float pressureIncreaseScale = 1.0 - ( float(invoker.GetPressure()) / float(KIRI_SPRAY_MAXPRESSURE)); pressureIncreaseScale *= pressureIncreaseScale; // Add pressure. int pressure = invoker.GetPressure(); pressure += random(0, KIRI_SPRAY_PRESSUREBUILDSCALE * invoker.weaponstatus[KIRI_SPRAY_WS_PAINT] / KIRI_SPRAY_MAXPAINT); // Cap pressure amount. if(pressure > KIRI_SPRAY_MAXPRESSURE) { pressure = KIRI_SPRAY_MAXPRESSURE; } invoker.SetPressure(pressure); } KSPR A 1 offset(0, 60) { A_OverlayRotate(0, sin(level.time * 5.0) * KIRI_SPRAY_SHAKEANIM_MAXANGLE * 2.0/3.0); } KSPR A 1 offset(0, 40) { A_OverlayRotate(0, sin(level.time * 5.0) * KIRI_SPRAY_SHAKEANIM_MAXANGLE * 1.0/3.0); } KSPR A 1 offset(0, 20) { A_OverlayRotate(0, 0); } goto ready; fire: KSPR A 2 { if(invoker.GetPressure() < KIRI_SPRAY_PRESSURE_PER_USE) { invoker.owner.A_Log("Not enough pressure to spray.", true); } else { float zOffset = GunHeight() * 0.8; FLineTraceData lineTraceData; class sprayerPatternClass = "SnekTechSprayerPattern"; // Find the spray pattern class that matches the // player's CVar for selected pattern. String currentSprayCVar = CVar.GetCVar( "snektech_spraypattern", invoker.owner.player).GetString(); for(int i = 0; i < AllActorClasses.size(); i++) { class thisPatternClass = (class)(AllActorClasses[i]); if(thisPatternClass) { if(thisPatternClass.GetClassName() == currentSprayCVar) { sprayerPatternClass = (class)(AllActorClasses[i]); break; } } } // Get the actual decal name out of it. let defaultSprayClass = GetDefaultByType(sprayerPatternClass); String actualDecalName = defaultSprayClass.decalName; // Find a wall to spray on. bool traceHit = LineTrace( angle, KIRI_SPRAY_DISTANCE, pitch, flags : TRF_THRUACTORS, offsetz : zOffset, data : lineTraceData); if(traceHit && lineTraceData.HitLine) { A_StartSound( "kiri/spraycan_spray", CHAN_WEAPON); let normal = lineTraceData.HitDir; let spawnedThing = Actor.Spawn("SnekTechSprayerDecalSpawner", lineTraceData.HitLocation - normal); SnekTechSprayerDecalSpawner spawner = SnekTechSprayerDecalSpawner(spawnedThing); if(spawner) { spawner.angle = VectorAngle(-normal.x, -normal.y); spawner.actualDecalName = actualDecalName; spawner.master = invoker.owner; invoker.SetPressure(invoker.GetPressure() - KIRI_SPRAY_PRESSURE_PER_USE); invoker.weaponstatus[KIRI_SPRAY_WS_PAINT] -= 1; } } else { invoker.owner.A_Log("Not close enough to a wall to spray.", true); } } } goto waiting; unload: KSPR A 2 { invoker.CycleSprayPattern(-1); } goto waiting; firemode: KSPR A 2 { invoker.CycleSprayPattern(1); } goto waiting; waiting: KSPR A 5; KSPR A 0 A_Refire("waiting"); goto ready; select0: KSPR A 0 offset(0, 120); ---- A 1 A_Raise(12); wait; deselect0: KSPR A 0; ---- A 1 A_Lower(12); wait; } override bool AddSpareWeapon(actor newowner) { return AddSpareWeaponRegular(newowner); } override HDWeapon GetSpareWeapon(actor newowner, bool reverse, bool doselect) { return GetSpareWeaponRegular(newowner, reverse, doselect); } // Why do we store pressure as this weird equation? // // Pressure is stored as a time since the a hypothetical last // "fully charged" time, based on the level time and pressure // decay rate. // // This way we can decay pressure without ticking, important for // spare weapons and stuff in backpacks. int GetPressure() const { int time = level.TotalTime; int lastChargedTime = weaponstatus[KIRI_SPRAY_WS_PRESSURE]; int pressure = int( float(KIRI_SPRAY_MAXPRESSURE) - float(time - lastChargedTime) * KIRI_SPRAY_PRESSURE_DECAY_RATE); if(pressure > KIRI_SPRAY_MAXPRESSURE) { pressure = KIRI_SPRAY_MAXPRESSURE; } if(pressure < 0) { pressure = 0; } return pressure; } void SetPressure(int pressure) { if(pressure > KIRI_SPRAY_MAXPRESSURE) { pressure = KIRI_SPRAY_MAXPRESSURE; } if(pressure < 0) { pressure = 0; } int time = level.TotalTime; int lastChargedTime = int(round( float(pressure - KIRI_SPRAY_MAXPRESSURE) / float(KIRI_SPRAY_PRESSURE_DECAY_RATE) + time)); weaponstatus[KIRI_SPRAY_WS_PRESSURE] = lastChargedTime; } void ResetPressure() { int resetValue = -int(float(KIRI_SPRAY_MAXPRESSURE) / float(KIRI_SPRAY_PRESSURE_DECAY_RATE)); weaponstatus[KIRI_SPRAY_WS_PRESSURE] = resetValue; } override void Consolidate() { ResetPressure(); } override void InitializeWepStats(bool idfa) { super.InitializeWepStats(idfa); SetPressure(0); // Add a little bit of randomness to the amount of paint in // the can. weaponstatus[KIRI_SPRAY_WS_PAINT] = KIRI_SPRAY_MAXPAINT - random(0, KIRI_SPRAY_MAXPAINT / 10); } override void DrawHUDStuff( HDStatusBar statusBar, HDWeapon weapon, HDPlayerPawn pawn) { // Current pressure (top bar). statusBar.DrawWepNum( GetPressure(), KIRI_SPRAY_MAXPRESSURE, posy:-10); // Total paint remaining (bottom bar). statusBar.DrawWepNum( weaponstatus[KIRI_SPRAY_WS_PAINT], KIRI_SPRAY_MAXPAINT); } void GetSprayPatternList(Array ret) { ret.clear(); for(int i = 0; i < AllActorClasses.Size(); i++) { class c = AllActorClasses[i]; if(c is "SnekTechSprayerPattern" && c != "SnekTechSprayerPattern") { ret.push(c.GetClassName()); } } } void CycleSprayPattern(int increment) { // Find the current entry in the list of patterns. String currentPattern = CVar.GetCVar("snektech_spraypattern", owner.player).GetString(); Array patternList; GetSprayPatternList(patternList); int currentIndex = patternList.find(currentPattern); // Make sure we're actually in the list (CVar might not be in // list, due to mods getting shuffled around and whatever). if(currentIndex >= patternList.size()) { currentIndex = -1; } // Increment/decrement the index. int newIndex = (currentIndex + increment) % patternList.size(); if(newIndex < 0) { newIndex = patternList.size() - 1; } // Set the new CVar. currentPattern = patternList[newIndex]; // first, update the Actual cvar if (owner.PlayerNumber() == consoleplayer) CVar.FindCVar("snektech_spraypattern").SetString(currentPattern); // then update the per-player copy so the help text is correct CVar.GetCVar("snektech_spraypattern", owner.player).SetString(currentPattern); // Print it. owner.A_Log( String.format( "Selected spray pattern: (%d/%d) %s", newIndex + 1, patternList.size(), currentPattern), true); // Current pattern is referenced in the help text, so reset // it. A_SetHelpText(); } override double WeaponBulk() { return 10; } override String GetHelpText() { super.GetHelpText(); int messageIndex = level.time; Array messages = { "Deface the tyrant's property.", "Engage in civil disobedience.", "Create a beautiful work of art.", "Show the world that you still exist.", "Defy the tyrant." }; String currentPattern = CVar.GetCVar("snektech_spraypattern", owner.player).GetString(); return WEPHELP_FIREMODE .. " Cycle pattern (current: "..currentPattern..").\n".. WEPHELP_UNLOAD .. " Cycle pattern (backwards).\n".. WEPHELP_ALTFIRE .. " Shake the can.\n".. WEPHELP_FIRE .. " "..messages[messageIndex % messages.Size()].."\n"; } } class SnekTechSprayerDecalSpawner : Decal { default { +nogravity; } states { spawn: TNT1 A -1; stop; } int thisSprayerId; String actualDecalName; Array decals; void SpawnDecal() { Thinker think; array seen; // find every decal that Isn't ours let iter = ThinkerIterator.Create('Thinker', STAT_DECAL); while (think = iter.Next()) { // BaseDecal isn't exported to zscript so we have to filter this manually if (think.GetClassName() == 'BaseDecal') { seen.Push(think); } } // SpawnDecal checks arg0 for the decal ID/name. // gzdoom passes string arguments as negated string table indices, so // we need to cast the decal name to a Name (implicitly adding it to // the string table), then to an int (to get the index). args[0] = -int(Name(actualDecalName)); Super.SpawnDecal(); // then find every decal that we spawned iter.Reinit(); while (think = iter.Next()) { if (think.GetClassName() == 'BaseDecal' && seen.Find(think) == seen.Size()) { decals.Push(think); } } } override void OnDestroy() { for (let i = 0; i < decals.Size(); i++) { if (decals[i]) { decals[i].Destroy(); } } } // Decal::BeginPlay deletes the actor override void BeginPlay() {Actor.BeginPlay();} override void PostBeginPlay() { Super.PostBeginPlay(); // copy A_SprayDecal z offset setz(pos.z + height / 2); SpawnDecal(); if (decals.Size() == 0) { Destroy(); return; } // Figure out a new ID number. ThinkerIterator iter = ThinkerIterator.Create("SnekTechSprayerDecalSpawner"); SnekTechSprayerDecalSpawner otherSpawner; int maxId = 0; while(otherSpawner = SnekTechSprayerDecalSpawner(iter.Next())) { if(otherSpawner == self) { continue; } // Keep track of the highest ID. if(otherSpawner.thisSprayerId > maxId) { if(otherSpawner.master == master) { maxId = otherSpawner.thisSprayerId; } } } thisSprayerId = maxId + 1; // Clear old sprayers. iter = ThinkerIterator.Create("SnekTechSprayerDecalSpawner"); while(otherSpawner = SnekTechSprayerDecalSpawner(iter.Next())) { if(otherSpawner == self) { continue; } int maxSpraysPerPlayer = CVar.GetCVar("snektech_maxspraysperplayer").GetInt(); if(otherSpawner.thisSprayerId <= (thisSprayerId - maxSpraysPerPlayer)) { if(otherSpawner.master == master) { otherSpawner.Destroy(); } } } } } class SnekTechSprayerPattern : Actor { string decalName; property decalName:decalName; default { SnekTechSprayerPattern.decalName "KiriTestDecal"; } } class SnekSpray_TransPride : SnekTechSprayerPattern { default { SnekTechSprayerPattern.decalName "SnekSpray_TransPride"; } } class SnekSpray_LesbianPride : SnekTechSprayerPattern { default { SnekTechSprayerPattern.decalName "SnekSpray_LesbianPride"; } } class SnekSpray_NBPride : SnekTechSprayerPattern { default { SnekTechSprayerPattern.decalName "SnekSpray_NBPride"; } } class SnekSpray_AcePride : SnekTechSprayerPattern { default { SnekTechSprayerPattern.decalName "SnekSpray_AcePride"; } } class SnekSpray_ProgressPride : SnekTechSprayerPattern { default { SnekTechSprayerPattern.decalName "SnekSpray_ProgressPride"; } } class SnekSpray_DemiPride : SnekTechSprayerPattern { default { SnekTechSprayerPattern.decalName "SnekSpray_DemiPride"; } } class SnekSpray_PanPride : SnekTechSprayerPattern { default { SnekTechSprayerPattern.decalName "SnekSpray_PanPride"; } } class SnekSpray_SwitchPride : SnekTechSprayerPattern { default { SnekTechSprayerPattern.decalName "SnekSpray_SwitchPride"; } } class SnekSpray_BiPride : SnekTechSprayerPattern { default { SnekTechSprayerPattern.decalName "SnekSpray_BiPride"; } } class SnekSpray_GayPride : SnekTechSprayerPattern { default { SnekTechSprayerPattern.decalName "SnekSpray_GayPride"; } }