diff --git a/credits.txt b/credits.txt index 2f4a401ed..c21bc0d64 100644 --- a/credits.txt +++ b/credits.txt @@ -19,6 +19,7 @@ Most of the work here is original, but there are some notable exceptions: - In addition, a whole lot of stock sounds and internet meme sounds have also been used. - Some sprites and sounds are taken from (shareware) Wolfenstein 3D. - This mod uses Gutamatics, by Gutawer. Big thanks. + - This mod uses Mikolah's ZPolyobject library. Many thanks too. - Title theme, "Traumatic State", by Teque (which a lot of people just know as "the AS-Golgotha music"). - Intermission theme, "Dragony", also by Teque (very comfy music considering the rest of his repertoire). - Startup/credits theme, "Hidden Tune #242", also by Teque too (super comfy music, ideal for this use). diff --git a/cvarinfo.base b/cvarinfo.base index 7bd1732d4..348a0f365 100644 --- a/cvarinfo.base +++ b/cvarinfo.base @@ -119,6 +119,7 @@ nosave bool swwm_nomapmsg = false; // disables special map dialogue messages nosave bool swwm_weapontooltips = true; // shows a tooltip on weapon operation when first selected nosave string swwm_tooltipshown = ""; // which weapons have already had their tooltips displayed nosave noarchive bool swwm_tooltipnote = false; // set if the note on how to disable tooltips was already displayed +nosave bool swwm_nointertips = false; // hides intermission tips, in case you don't want to see them // minimap settings nosave bool swwm_mm_enable = true; // show a minimap below the score counter diff --git a/language.def_menu b/language.def_menu index 650b92e38..6ba09d025 100644 --- a/language.def_menu +++ b/language.def_menu @@ -204,6 +204,7 @@ SWWM_WEAPONTOOLTIPS = "Weapon Tooltips"; SWWM_RESETTOOLTIPS = "Reset Weapon Tooltips"; SWWM_CRESET = "Reset to Defaults"; SWWM_ARESET = "Wipe Achievements"; +SWWM_NOINTERTIPS = "Hide Intermission Tips"; SWWM_MMTITLE = "Minimap Settings"; SWWM_MM_ENABLE = "Show Minimap"; SWWM_MM_ROTATE = "Rotate Minimap"; @@ -359,6 +360,7 @@ TOOLTIP_SWWM_WEAPONTOOLTIPS = "Shows a summary of controls when a weapon is sele TOOLTIP_EVENT_SWWMRESETTOOLTIPS = "Resets weapon tooltips so you can see them all over again."; TOOLTIP_EVENT_SWWMRESETCVARS = "Resets all the mod's settings to their original values."; TOOLTIP_EVENT_SWWMRESETACHIEVEMENTS = "Erases all your archievement progress."; +TOOLTIP_SWWM_NOINTERTIPS = "Hides intermission tips, in case you don't want to see them."; TOOLTIP_SWWMMINIMAPMENU = "Configure the minimap."; TOOLTIP_SWWMACHIEVEMENTMENU = "View your achievements."; TOOLTIP_SWWM_MM_ENABLE = "Displays a minimap under the score counter."; @@ -414,6 +416,7 @@ SWWM_CMAB2 = "For being an amazing friend who believes in me, and for inspiring SWWM_CDRAGON2 = "For being a good pet dragon who cares about me."; SWWM_CLUCY2 = "For the Tewi font, which I've used for many many years. I hope you're doing well, wherever you are."; SWWM_CGUTA2 = "For the Gutamatics library, and for helping me with learning ZScript."; +SWWM_CMIKO2 = "For the VERY useful ZPolyobject library."; SWWM_CKEKS2 = "For assistance with exception handling code, and also for being such a cool Touhou nerd."; SWWM_CZN2 = "For slope alignment code, and to Nash also for being a cool smart cactus dude."; SWWM_CVAL2 = "For the custom Nashgore footprints, and for being a good friend and cute bun."; diff --git a/language.es_menu b/language.es_menu index c3bbf458d..b939105f0 100644 --- a/language.es_menu +++ b/language.es_menu @@ -203,6 +203,7 @@ SWWM_WEAPONTOOLTIPS = "Ayuda de Armas"; SWWM_RESETTOOLTIPS = "Resetear Ayuda de Armas"; SWWM_CRESET = "Restaurar Predeterminado"; SWWM_ARESET = "Borrar Logros"; +SWWM_NOINTERTIPS = "Ocultar Consejos de Intermisión"; SWWM_MMTITLE = "Opciones de Minimapa"; SWWM_MM_ENABLE = "Mostrar Minimapa"; SWWM_MM_ROTATE = "Rotar Minimapa"; @@ -358,6 +359,7 @@ TOOLTIP_SWWM_WEAPONTOOLTIPS = "Muestra un resumen de controles cuando se selecci TOOLTIP_EVENT_SWWMRESETTOOLTIPS = "Resetea la ayuda de armas para que puedas volver a verla de nuevo."; TOOLTIP_EVENT_SWWMRESETCVARS = "Resetea todas las opciones del mod a sus valores originales."; TOOLTIP_EVENT_SWWMRESETACHIEVEMENTS = "Borra todo tu progreso de logros."; +TOOLTIP_SWWM_NOINTERTIPS = "Oculta los consejos de intermisión, por si no quieres verlos."; TOOLTIP_SWWMMINIMAPMENU = "Configura el minimapa."; TOOLTIP_SWWMACHIEVEMENTMENU = "Revisa tus logros."; TOOLTIP_SWWM_MM_ENABLE = "Muestra un minimapa bajo el contador de puntuación."; @@ -411,6 +413,7 @@ SWWM_CMAB2 = "Por ser una grandísima amiga que cree en mí, y por inspirarme a SWWM_CDRAGON2 = "Por ser un buen dragón mascota que se preocupa por mí."; SWWM_CLUCY2 = "Por la fuente Tewi, que he seguido usando todos estos años. Espero que estés donde estés, te encuentres bien."; SWWM_CGUTA2 = "Por la librería de Gutamatics, y por ayudarme a aprender ZScript."; +SWWM_CMIKO2 = "Por la MUY útil librería de ZPolyobject."; SWWM_CKEKS2 = "Por asistencia con el código de manejo de excepciones, y también por ser un friki tan guay de Touhou."; SWWM_CZN2 = "Por el código para alineación con superficies inclinadas, y a Nash además por ser un molón tío cactus listo."; SWWM_CVAL2 = "Por las huellas personalizadas para Nashgore, y por ser una buena amiga y conejita mona."; diff --git a/language.version b/language.version index 9c3adad24..9623bbcaa 100644 --- a/language.version +++ b/language.version @@ -1,3 +1,3 @@ [default] -SWWM_MODVER="\chSWWM \czGZ\c- \cw1.2pre r4 \cu(Mon 27 Sep 12:31:19 CEST 2021)\c-"; -SWWM_SHORTVER="\cw1.2pre r4 \cu(2021-09-27 12:31:19)\c-"; +SWWM_MODVER="\chSWWM \czGZ\c- \cw1.2pre r5 \cu(Wed 29 Sep 18:47:23 CEST 2021)\c-"; +SWWM_SHORTVER="\cw1.2pre r5 \cu(2021-09-29 18:47:23)\c-"; diff --git a/menudef.txt b/menudef.txt index 7fa48e48d..5867bc251 100644 --- a/menudef.txt +++ b/menudef.txt @@ -135,6 +135,7 @@ OptionMenu "SWWMOptionMenu" Option "$SWWM_NUMCOLOR_HP", "swwm_numcolor_hp", "TextColors" Option "$SWWM_NUMCOLOR_AP", "swwm_numcolor_ap", "TextColors" Option "$SWWM_INTERART", "swwm_interart", "YesNo" + Option "$SWWM_NOINTERTIPS", "swwm_nointertips", "YesNo" Option "$SWWM_INTERMUSIC", "swwm_intermusic", "YesNo" Option "$SWWM_FUZZ", "swwm_fuzz", "YesNo" Option "$SWWM_SILENCEMAP", "swwm_silencemap", "YesNo" diff --git a/zscript.txt b/zscript.txt index 8d89197aa..a27512df9 100644 --- a/zscript.txt +++ b/zscript.txt @@ -9,6 +9,8 @@ version "4.6" // Gutamatics #include "zscript/swwm_Gutamatics/Include.zsc" +// ZPolyobject +#include "zscript/swwm_Polyobjects/Polyobjects.zs" // utility code #include "zscript/utility/swwm_coordutil.zsc" #include "zscript/utility/swwm_utility.zsc" diff --git a/zscript/compat/swwm_compat.zsc b/zscript/compat/swwm_compat.zsc index 47e2a8ae8..a498bf428 100644 --- a/zscript/compat/swwm_compat.zsc +++ b/zscript/compat/swwm_compat.zsc @@ -332,7 +332,7 @@ Class SWWMLevelCompatibility : LevelPostProcessor case '959A613006CC3AA912C4A22908B7566A': // add collectibles for ( int i=0; i<12; i++ ) - AddThing(4206900+i,(1472+64*i,640,0),SKILLS_ALL,MTF_SINGLE|MTF_COOPERATIVE); + AddThing(4206900+i,(1472+64*i,640,0),0,SKILLS_ALL,MTF_SINGLE|MTF_COOPERATIVE); // add some chance boxes for ( int i=0; i<3; i++ ) AddThing(4206920,(3616,1824-64*i,0),180,SKILLS_ALL,MTF_SINGLE|MTF_COOPERATIVE); diff --git a/zscript/items/swwm_ammoextra.zsc b/zscript/items/swwm_ammoextra.zsc index afd7135e3..0ef80dabe 100644 --- a/zscript/items/swwm_ammoextra.zsc +++ b/zscript/items/swwm_ammoextra.zsc @@ -477,7 +477,7 @@ Class HammerspaceEmbiggener : Inventory i.MaxAmount = int(i.default.MaxAmount+Amount*factor); } int amount = Ammo(i).BackpackAmount*item.Amount; - if ( traded ) i.Amount = 0; + if ( traded ) amount = 0; if ( (i.Amount > 0) && (i.Amount+amount < 0) ) i.Amount = int.max; else i.Amount += amount; diff --git a/zscript/items/swwm_funstuff.zsc b/zscript/items/swwm_funstuff.zsc index 1d0fa4474..054ee693d 100644 --- a/zscript/items/swwm_funstuff.zsc +++ b/zscript/items/swwm_funstuff.zsc @@ -719,6 +719,9 @@ Class ChanceboxSpawner : Actor if ( deathmatch ) { // not in DM + let b = Spawn("HealthNuggetItem",pos); + SWWMUtility.TransferItemProp(self,b); + ClearCounters(); Destroy(); return; } @@ -728,6 +731,9 @@ Class ChanceboxSpawner : Actor if ( numbox >= 3 ) { // there's three boxes in the map already + let b = Spawn("HealthNuggetItem",pos); + SWWMUtility.TransferItemProp(self,b); + ClearCounters(); Destroy(); return; } @@ -748,6 +754,9 @@ Class ChanceboxSpawner : Actor if ( tbox[1] > l.bbox[0] ) continue; if ( SWWMUtility.BoxOnLineSide(tbox[0],tbox[1],tbox[2],tbox[3],l) != -1 ) continue; // there isn't enough space to spawn a box here + let b = Spawn("HealthNuggetItem",pos); + SWWMUtility.TransferItemProp(self,b); + ClearCounters(); Destroy(); return; } diff --git a/zscript/items/swwm_powerups.zsc b/zscript/items/swwm_powerups.zsc index 9b2208abe..b6d604498 100644 --- a/zscript/items/swwm_powerups.zsc +++ b/zscript/items/swwm_powerups.zsc @@ -3301,7 +3301,8 @@ Class AngeryPower : Powerup // (2^31-1)/25 : guarantee that it caps rather than overflowing if ( damage > 85899345 ) newdamage = int.max; else newdamage = damage*25; - DoHitFX(); + // don't play hit fx for wall busting, as it'll be done manually if the bust goes through + if ( damageType != 'Wallbust' ) DoHitFX(); } } Class AngerySigil : Inventory diff --git a/zscript/menu/swwm_credits.zsc b/zscript/menu/swwm_credits.zsc index ede5d1e33..a715c0d71 100644 --- a/zscript/menu/swwm_credits.zsc +++ b/zscript/menu/swwm_credits.zsc @@ -184,18 +184,15 @@ Class SWWMCreditsMenu : GenericMenu cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Dac")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Pietro Gagliardi")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Ryan Weatherman")); - cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Valerie Thiessen")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Xada Xephron")); - cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Zard1084")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"John")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"VoanHead")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"NekoMithos")); - cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Artem Bashev")); + cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Ceyne Taikato")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"bouncytem")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Brett Saltzer")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Clint Walker")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Figo")); - cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Holly_Rook")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"m8f")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"Namsan")); cpatrons.Push(new("SWWMCreditsEntry").Init(sfnt,"YaGirlJuniper")); @@ -203,6 +200,7 @@ Class SWWMCreditsMenu : GenericMenu cthanks.Push(new("SWWMCreditsEntry").Init(sfnt,"KynikossDragonn","$SWWM_CDRAGON2")); cthanks.Push(new("SWWMCreditsEntry").Init(sfnt,"Lucy","$SWWM_CLUCY2")); cthanks.Push(new("SWWMCreditsEntry").Init(sfnt,"Gutawer","$SWWM_CGUTA2")); + cthanks.Push(new("SWWMCreditsEntry").Init(sfnt,"Mikolah","$SWWM_CMIKO2")); cthanks.Push(new("SWWMCreditsEntry").Init(sfnt,"KeksDose","$SWWM_CKEKS2")); cthanks.Push(new("SWWMCreditsEntry").Init(sfnt,"ZZYZX & Nash","$SWWM_CZN2")); cthanks.Push(new("SWWMCreditsEntry").Init(sfnt,"Val Pal","$SWWM_CVAL2")); diff --git a/zscript/menu/swwm_inter.zsc b/zscript/menu/swwm_inter.zsc index 4ebee2322..8b3025d83 100644 --- a/zscript/menu/swwm_inter.zsc +++ b/zscript/menu/swwm_inter.zsc @@ -145,6 +145,7 @@ Class SWWMStatScreen : StatusScreen abstract whichtip = ents[Random[InterArt](0,ents.Size()-1)]; pdata.lasttip.Push(whichtip); } + if ( swwm_nointertips ) return; String tipstr = "\cd"..String.Format(StringTable.Localize("$SWWM_INTERTIP"),whichtip).."\c-\n"..StringTable.Localize(String.Format("$SWWM_INTERTIP%d",whichtip)); BrokenLines l = fnt.BreakLines(tipstr,400); int lw = 0; diff --git a/zscript/swwm_Polyobjects/PolyobjectEffector.zs b/zscript/swwm_Polyobjects/PolyobjectEffector.zs new file mode 100644 index 000000000..445df752c --- /dev/null +++ b/zscript/swwm_Polyobjects/PolyobjectEffector.zs @@ -0,0 +1,48 @@ +class swwm_PolyobjectEffector: Thinker abstract +{ + // Base abstract class for Polyobject Effectors + // Polyobject Effectors affect how a polyobject behaves. + // Polyobject Effectors contain a pointer to the next Effector of a polyobject, + // forming a circular linked list. + // To add an effector to a polyobject, call AddEffector() on a PolyobjectHandle. + // To remove an effector, simply call Destroy() on it. + + swwm_PolyobjectHandle Polyobject; + swwm_PolyobjectEffector Next; + + // OnAdd() is called once after adding the effector to a PolyobjectHandle + virtual void OnAdd() + { + + } + + // PolyTick() is called every tic by a PolyobjectHandle + virtual void PolyTick() + { + + } + + override void OnDestroy() + { + swwm_PolyobjectEffector e = Polyobject.EffectorList; + if (e != NULL) + { + // Find previous effector + while (e && e.Next != self) + { + e = e.Next; + } + + // Link previous effector to the next effector + e.Next = Next; + + // Check if this effector is the last one + if (e == self) + { + // Polyobject has no other effectors, set EffectorList to NULL + Polyobject.EffectorList = NULL; + } + } + Super.OnDestroy(); + } +} diff --git a/zscript/swwm_Polyobjects/PolyobjectHandle.zs b/zscript/swwm_Polyobjects/PolyobjectHandle.zs new file mode 100644 index 000000000..23383f231 --- /dev/null +++ b/zscript/swwm_Polyobjects/PolyobjectHandle.zs @@ -0,0 +1,359 @@ +class swwm_PolyobjectHandle: Thinker +{ + // This thinker keeps track of a polyobject's position, angle, movement speed etc. + // Instances of this thinker should only be created by the included map postprocessor + + enum EPolyobjType + { + POTYP_NORMAL = 9301, // Normal StartSpot + POTYP_CRUSH = 9302, // Crush StartSpot + POTYP_HURT = 9303 // Hurt StartSpot + } + + // Polyobject Number + int PolyobjectNum; + + // Line defining the polyobject (Polyobj_StartLine, or one of Polyobj_ExplicitLine) + Line StartLine; + + // [MK] All lines belonging to the polyobject + Array Lines; + + // Initial angle of StartLine + double StartAngle; + + // Last tic angle of StartLine + double LastAngle; + + // Starting positions of StartLine vertices + Vector2[2] VertexStartingPos; + + // Last tic positions of StartLine vertices + Vector2[2] VertexLastPos; + + // Last tic position of the polyobject + Vector2 LastPos; + + // SoundSequence number + int SoundSequenceNum; + + // StartSpot position + Vector2 StartSpotPos; + + // For bounds checking + double z; + + // Sector the polyobject spawns in () + Sector StartSector; + + // Polyobject type (normal, crush, hurt) + EPolyobjType Type; + + // Mirror Polyobject + swwm_PolyobjectHandle Mirror; + + // Circular linked list of attached Polyobject Effectors + swwm_PolyobjectEffector EffectorList; + + // Whether the initalization has finished + bool IsInitialized; + + // Creates a PolyobjectHandle + static swwm_PolyobjectHandle Create() + { + swwm_PolyobjectHandle po = swwm_PolyobjectHandle(new('swwm_PolyobjectHandle')); + // Sets the underlying Thinker StatNum to 127 so that LastPos, LastAngle etc. get + // updated after all the other thinkers. + po.ChangeStatNum(127); + + return po; + } + + // Returns a PolyobjectHandle corresponding to the provided polyobject number + // Returns NULL if no such handler exists. + static swwm_PolyobjectHandle FindPolyobj(int pobjnum) + { + swwm_PolyobjectHandle po; + let it = ThinkerIterator.Create('swwm_PolyobjectHandle'); + while ((po = swwm_PolyobjectHandle(it.Next())) != NULL) + { + if (po.PolyobjectNum == pobjnum) + return po; + } + return NULL; + } + + // Adds effector to the end of current effector list + void AddEffector(swwm_PolyobjectEffector effector) + { + + effector.Polyobject = self; + + // If effector list is empty, new effector becomes head of the list + if (EffectorList == NULL) + { + EffectorList = effector; + effector.Next = effector; + } + else + { + swwm_PolyobjectEffector e = EffectorList; + // Go through every effector until the last item + while (e.Next != EffectorList) + { + e = e.Next; + } + effector.Next = EffectorList; + e.Next = effector; + } + if (IsInitialized) + { + // If we're initialized, run the OnAdd effect immediately + effector.OnAdd(); + } + } + + // Finds effector of specified class, and returns it + swwm_PolyobjectEffector FindEffector(class effectorclass) + { + // No effectors? Nothing to find then + if (EffectorList == NULL) + return NULL; + + // Go through each effector + swwm_PolyobjectEffector e = EffectorList; + do + { + + // Effector is of specified class, return it + if (e is effectorclass) + return e; + + e = e.Next; + } + while (e != EffectorList); + return NULL; + } + + override void PostBeginPlay() + { + // Initialization shouldn't happen when reentering the map + if (Level.Time > 0) + return; + + // Map has no lines corresponding to this polyobject, destroy the handle + if (StartLine == NULL) + { + Destroy(); + return; + } + + // Using polyobject linedefs is the only way to track polyobject movements. + // All geometric calculations will be done relative to StartLine. + + // Store initial position and angle + StartAngle = VectorAngle(StartLine.Delta.x, StartLine.Delta.y); + VertexStartingPos[0] = VertexLastPos[0] = StartLine.v1.p; + VertexStartingPos[1] = VertexLastPos[1] = StartLine.v2.p; + + // Now that the map is loaded, it's safe to call effectors' OnAdd() methods + swwm_PolyobjectEffector e = EffectorList; + if (e != NULL) + { + do + { + e.OnAdd(); + e = e.Next; + } + while (e != EffectorList); + } + + // Done initializing + IsInitialized = true; + } + + override void Tick() + { + // Call PolyTick() for each effector + if (EffectorList != NULL) + { + swwm_PolyobjectEffector e = EffectorList; + do + { + e.PolyTick(); + e = e.Next; + } + while (e != EffectorList); + } + + // Store current position/angle to be used during the next tic + VertexLastPos[0] = StartLine.v1.p; + VertexLastPos[1] = StartLine.v2.p; + LastAngle = GetAngle(); + LastPos = GetPos(); + } + + override void OnDestroy() + { + // Clean up effectors first + swwm_PolyobjectEffector e = EffectorList; + if (e != NULL) + { + swwm_PolyobjectEffector next; + do + { + next = e.Next; + e.Destroy(); + e = next; + } + while (e != EffectorList); + } + } + + Sector GetSector() + { + if (StartSector == NULL) + { + Vector2 SpotPos = StartSpotPos; + + // Sometimes if StartSpot lies on a one-sided linedef, its position is considered + // out of bounds by GZDoom, which makes Level.PointInSector() produce unexpected + // results. In that case, we need to compensate. + if (!Level.IsPointInLevel((SpotPos, z))) + { + // Look at points in a 5x5 square around the StartSpot + for (int x = -2; x <= 2; x++) + { + for (int y = -2; y <= 2; y++) + { + SpotPos = StartSpotPos + (x, y); + + if (Level.IsPointInLevel((SpotPos, z))) + { + // Found a point within bounds, should be good enough + StartSector = Level.PointInSector(SpotPos); + break; + } + } + if (StartSector) + break; + } + } + else + { + StartSector = Level.PointInSector(SpotPos); + } + } + return StartSector; + } + + // Returns initial StartSpot position + Vector2 GetOrigin() + { + return StartSpotPos; + } + + // Returns current polyobject angle + double GetAngle() + { + double lineangle = VectorAngle(StartLine.Delta.x, StartLine.Delta.y); + return Actor.DeltaAngle(StartAngle, lineangle); + } + + // Returns current polyobject startspot position + Vector2 GetPos() + { + let spotdelta = StartSpotPos - VertexStartingPos[0]; + return StartLine.v1.p + Actor.RotateVector(spotdelta, GetAngle()); + } + + // Returns polyobject coordinates relative to the startspot + Vector2 GetPosDelta() + { + return GetPos() - StartSpotPos; + } + + // Returns last polyobject angle + double GetLastAngle() + { + return LastAngle; + } + + // Returns last polyobject startspot position + Vector2 GetLastPos() + { + return LastPos; + } + + // Returns last coordinates relative to the startspot + Vector2 GetLastPosDelta() + { + return LastPos - StartSpotPos; + } + + // Returns current polyobject velocity + Vector2 GetVel() + { + return StartLine.v1.p - VertexLastPos[0]; + } + + // Returns current polyobject rotation speed + double GetRotationSpeed() + { + return GetAngle() - LastAngle; + } + + // Returns whether the polyobject has moved from its spawn position + bool IsAtOrigin() + { + return (VertexStartingPos[0] == StartLine.v1.p && VertexStartingPos[1] == StartLine.v2.p); + } + + // Returns whether the polyobject is in motion + bool IsMoving() + { + return (GetPos() != GetLastPos() || GetAngle() != GetLastAngle()); + } + + // Moves the polyobject to specified location, with specified speed, and plays the + // specified sound of its sound sequence + // (i.e. for a door sound sequence, sndseqmode 0 plays the open sound, 1 plays the + // closing sound) + void MoveTo(Actor activator, Vector2 dest, int Speed, int sndseqmode = 0) + { + // Stop any polyobject movement + Level.ExecuteSpecial(Polyobj_Stop, activator, StartLine, Line.Front, PolyobjectNum); + // Move the polyobject + Level.ExecuteSpecial(Polyobj_OR_MoveTo, activator, StartLine, Line.Front, PolyobjectNum, + Speed, dest.x, dest.y); + + // Polyobj_OR_MoveTo ignores the sound sequence set by the polyobject. + // Play the sound sequence manually inside the sector containing the polyobject. + if (SoundSequenceNum) + { + GetSector().StartSoundSequenceID(CHAN_AUTO, SoundSequenceNum, SeqNode.DOOR, sndseqmode, false); + } + } +} + +// Class for iterating over polyobjects +class swwm_PolyobjectIterator: Object +{ + private ThinkerIterator it; + static swwm_PolyobjectIterator Create() + { + let it = New('swwm_PolyobjectIterator'); + it.it = ThinkerIterator.Create('swwm_PolyobjectHandle'); + return it; + } + + swwm_PolyobjectHandle Next() + { + return swwm_PolyobjectHandle(it.Next()); + } + + void Reinit() + { + it.Reinit(); + } +} diff --git a/zscript/swwm_Polyobjects/PolyobjectMapPostprocessor.zs b/zscript/swwm_Polyobjects/PolyobjectMapPostprocessor.zs new file mode 100644 index 000000000..13f78632f --- /dev/null +++ b/zscript/swwm_Polyobjects/PolyobjectMapPostprocessor.zs @@ -0,0 +1,123 @@ +// Creates a PolyobjectHandle for every polyobject in the map +class swwm_PolyobjectHandlePostProcessor: LevelPostProcessor +{ + protected void Apply(Name checksum, String mapname) + { + Array pobjnums; + Array pobjhandles; + + // Make sure initialization doesn't happen when reentering a map + if (Level.Time > 0) + return; + + // Look for Polyobject StartSpots and create a handle for each + for (int i = 0; i < GetThingCount(); i++) + { + // Ignore every thing that isn't a Polyobject StartSpot + int ednum = GetThingEdNum(i); + // [MK] hotfix for this to recognize hexen polyobjects + if (gameinfo.gametype&GAME_Hexen) + { + if (ednum == 3001) ednum = swwm_PolyobjectHandle.POTYP_NORMAL; + else if (ednum == 3002) ednum = swwm_PolyobjectHandle.POTYP_CRUSH; + } + if (ednum < swwm_PolyobjectHandle.POTYP_NORMAL || ednum > swwm_PolyobjectHandle.POTYP_HURT) + continue; + + // Create a PolyobjectHandle + swwm_PolyobjectHandle handle = swwm_PolyobjectHandle.Create(); + + // Get polyobject number from StartSpot angle + handle.PolyobjectNum = GetThingAngle(i); + + // Store StartSpot position + Vector3 pos = GetThingPos(i); + handle.StartSpotPos = pos.xy; + handle.z = pos.z; + + // Store StartSpot type (normal, crush, hurt) + handle.Type = ednum; + + // Append polyobject number and corresponding handle to the respective arrays + pobjnums.Push(handle.PolyobjectNum); + pobjhandles.Push(handle); + } + + // Look for Polyobj_StartLine/Polyobj_ExplicitLine lines + for (int i = 0; i < Level.Lines.Size(); i++) + { + Line line = Level.Lines[i]; + + // Ignore every line that doesn't have a Polyobj_StartLine or Polyobj_ExplicitLine + // line special + if (line.Special != Polyobj_StartLine && line.Special != Polyobj_ExplicitLine) + continue; + + // Get polyobject number + // (Args[0] for both Polyobj_StartLine and Polyobj_ExplicitLine) + int pobjnum = line.Args[0]; + + // Find the array index of the corresponding handle + int pobjhandleindex = pobjnums.Find(pobjnum); + if (pobjhandleindex >= pobjnums.Size()) + continue; // Polyobject doesn't have a corresponding StartSpot + + swwm_PolyobjectHandle handle = pobjhandles[pobjhandleindex]; + + // Get mirror polyobject number + // (Args[1] for Polyobj_StartLine, Args[2] for Polyobj_ExplicitLine) + int mirrorpobjnum = line.Special == Polyobj_StartLine ? line.Args[1] : line.Args[2]; + if (mirrorpobjnum != 0) + { + // Find the array index of the mirror polyobject handle + int mirrorpobjhandleindex = pobjnums.Find(mirrorpobjnum); + if (mirrorpobjhandleindex < pobjnums.Size()) + { + // Mirror polyobject handle exists, store it + handle.Mirror = pobjhandles[mirrorpobjhandleindex]; + } + } + + // Get sound sequence number and store it + // (Args[2] for Polyobj_StartLine, Args[3] for Polyobj_ExplicitLine) + int soundseq = line.Special == Polyobj_StartLine ? line.Args[2] : line.Args[3]; + handle.SoundSequenceNum = soundseq; + + // Store the line + handle.StartLine = line; + + // [MK] the library doesn't store ALL lines belonging to the polyobject, but we need them + handle.Lines.Push(line); + + // [MK] collect all connected lines if this is Polyobj_StartLine + if ( line.Special != Polyobj_StartLine ) + continue; + + bool newlines; + do + { + newlines = false; + for (int j = 0; j < Level.Lines.Size(); j++) + { + Line linea = Level.Lines[j]; + if (handle.Lines.Find(linea) < handle.Lines.Size()) + continue; + bool nomatches = true; + for (int k = 0; k < handle.Lines.Size(); k++) + { + Line lineb = handle.Lines[k]; + if ((linea.v1 != lineb.v1) && (linea.v1 != lineb.v2) && (linea.v2 != lineb.v1) && (linea.v2 != lineb.v2)) + continue; + nomatches = false; + break; + } + if (nomatches) + continue; + newlines = true; + handle.Lines.Push(linea); + } + } + while (newlines); + } + } +} diff --git a/zscript/swwm_Polyobjects/Polyobjects.zs b/zscript/swwm_Polyobjects/Polyobjects.zs new file mode 100644 index 000000000..56460e605 --- /dev/null +++ b/zscript/swwm_Polyobjects/Polyobjects.zs @@ -0,0 +1,3 @@ +#include "zscript/swwm_Polyobjects/PolyobjectHandle.zs" +#include "zscript/swwm_Polyobjects/PolyobjectMapPostprocessor.zs" +#include "zscript/swwm_Polyobjects/PolyobjectEffector.zs" diff --git a/zscript/swwm_player.zsc b/zscript/swwm_player.zsc index 1ae0118da..06c888b84 100644 --- a/zscript/swwm_player.zsc +++ b/zscript/swwm_player.zsc @@ -1579,6 +1579,47 @@ Class Demolitionist : PlayerPawn } SWWMUtility.MarkAchievement("crush",player); } + private void CheckBreakPolyobject( int dmg ) + { + // see if there are any crushing polyobjects currently "encroaching" the player + Array touching; + BlockLinesIterator bl = BlockLinesIterator.Create(self,radius+8); + double tbox[4]; + // top, bottom, left, right + tbox[0] = pos.y+(radius+8); + tbox[1] = pos.y-(radius+8); + tbox[2] = pos.x-(radius+8); + tbox[3] = pos.x+(radius+8); + while ( bl.Next() ) + { + Line l = bl.CurLine; + if ( !l ) continue; + if ( tbox[2] > l.bbox[3] ) continue; + if ( tbox[3] < l.bbox[2] ) continue; + if ( tbox[0] < l.bbox[1] ) continue; + if ( tbox[1] > l.bbox[0] ) continue; + if ( SWWMUtility.BoxOnLineSide(tbox[0],tbox[1],tbox[2],tbox[3],l) != -1 ) continue; + touching.Push(l); + } + let pi = swwm_PolyobjectIterator.Create(); + swwm_PolyobjectHandle p; + while ( p = pi.Next() ) + { + if ( (p.Type != swwm_PolyobjectHandle.POTYP_CRUSH) && (p.Type != swwm_PolyobjectHandle.POTYP_HURT) ) + continue; + for ( int i=0; i= p.Lines.Size() ) continue; + Vector2 diragainst = pos.xy-p.GetPos(); + double vsiz = diragainst.length(); + if ( vsiz > 0 ) diragainst /= vsiz; + if ( BusterWall.BustPolyobj(p,max(dmg,(100-health)*5),self,(diragainst.x,diragainst.y,0)) ) + SWWMUtility.MarkAchievement('swwm_achievement_crush',player); + if ( p.Mirror && BusterWall.BustPolyobj(p.Mirror,max(dmg,(100-health)*5),self,-(diragainst.x,diragainst.y,0)) ) + SWWMUtility.MarkAchievement('swwm_achievement_crush',player); + } + } + } override int DamageMobj( Actor inflictor, Actor source, int damage, Name mod, int flags, double angle ) { // we still have to ENSURE ENTIRELY that this gets nullified (TELEFRAG_DAMAGE overrides damage factors somehow) @@ -1592,7 +1633,12 @@ Class Demolitionist : PlayerPawn if ( mod == 'Crush' ) { // check if we can break any active crushers - if ( !inflictor && !source ) CheckBreakCrusher(); + // (or polyobjects) + if ( !inflictor && !source ) + { + CheckBreakCrusher(); + CheckBreakPolyobject(damage); + } // break a spike trap else if ( source is 'ThrustFloor' ) { diff --git a/zscript/swwm_thinkers.zsc b/zscript/swwm_thinkers.zsc index 7b8667b66..510182215 100644 --- a/zscript/swwm_thinkers.zsc +++ b/zscript/swwm_thinkers.zsc @@ -151,16 +151,32 @@ Class SWWMDamageAccumulator : Thinker } } +// ensures a polyobj stays out of bounds FOREVER +Class SWWMBustedPolyobj : swwm_PolyobjectEffector +{ + Actor whomstdve; + + override void PolyTick() + { + if ( Polyobject.GetPos() == (32000,32000) ) return; + double dist = (Polyobject.GetPos()-(32000,32000)).length(); + Level.ExecuteSpecial(Polyobj_Stop,whomstdve,Polyobject.StartLine,Line.Front,Polyobject.PolyobjectNum); + if ( Polyobject.Mirror ) Level.ExecuteSpecial(Polyobj_Stop,whomstdve,Polyobject.Mirror.StartLine,Line.Front,Polyobject.Mirror.PolyobjectNum); + Level.ExecuteSpecial(Polyobj_MoveTo,whomstdve,Polyobject.StartLine,Line.Front,Polyobject.PolyobjectNum,int(dist*8),32000,32000); + if ( Polyobject.Mirror ) Level.ExecuteSpecial(Polyobj_Stop,whomstdve,Polyobject.Mirror.StartLine,Line.Front,Polyobject.Mirror.PolyobjectNum); + } +} + // prevents floors/ceilings from ever moving again, as they're "broken crushers" -// optional "instant" parameter is used by wall busting, no crusher malfunction animation will play Class SWWMCrusherBroken : Thinker { Sector fsec, csec; double diffh; int fphase, cphase; int ftics, ctics; + SectorEffect fse, cse; // pointers to zero-speed movers - static void Create( Sector f, Sector c, double diffh, bool instant = false ) + static void Create( Sector f, Sector c, double diffh ) { if ( !f && !c ) return; let ti = ThinkerIterator.Create("SWWMCrusherBroken",STAT_USER); @@ -168,15 +184,7 @@ Class SWWMCrusherBroken : Thinker while ( cb = SWWMCrusherBroken(ti.Next()) ) { if ( (cb.fsec == f) && (cb.csec == c) ) - { - if ( instant ) - { - // force it to be instant - if ( f ) cb.fphase = 3; - if ( c ) cb.cphase = 3; - } return; // we already have this - } if ( cb.fsec && (cb.fsec == f) ) { cb.Destroy(); // we override this one @@ -195,10 +203,21 @@ Class SWWMCrusherBroken : Thinker cb.diffh = diffh; if ( f && f.floordata ) f.floordata.Destroy(); if ( c && c.ceilingdata ) c.ceilingdata.Destroy(); - if ( instant ) + } + + static void Remove( Sector f, Sector c ) + { + if ( !f && !c ) return; + let ti = ThinkerIterator.Create("SWWMCrusherBroken",STAT_USER); + SWWMCrusherBroken cb; + while ( cb = SWWMCrusherBroken(ti.Next()) ) { - if ( f ) cb.fphase = 3; - if ( c ) cb.cphase = 3; + if ( (cb.fsec == f) && (cb.csec == c) ) + cb.Destroy(); // destroy entirely + else if ( f && (cb.fsec == f) ) + cb.fsec = null; // only clear the floor + else if ( c && (cb.csec == c) ) + cb.csec = null; // only clear the ceiling } } @@ -228,10 +247,12 @@ Class SWWMCrusherBroken : Thinker ftics--; if ( ftics <= 0 ) fphase = 3; } - else if ( (fphase >= 3) && fsec.floordata ) + else if ( (fphase >= 3) && (!fse || (fsec.floordata != fse)) ) { - fsec.floordata.Destroy(); + if ( fsec.floordata ) fsec.floordata.Destroy(); + level.CreateFloor(fsec,Floor.floorLowerByValue,null,0.,1.); fsec.StopSoundSequence(CHAN_WEAPON); + fse = fsec.floordata; } } if ( csec ) @@ -258,10 +279,12 @@ Class SWWMCrusherBroken : Thinker ctics--; if ( ctics <= 0 ) cphase = 3; } - else if ( (cphase >= 3) && csec.ceilingdata ) + else if ( (cphase >= 3) && (!cse || (csec.ceilingdata != cse)) ) { - csec.ceilingdata.Destroy(); + if ( csec.ceilingdata ) csec.ceilingdata.Destroy(); + level.CreateCeiling(csec,Ceiling.ceilRaiseByValue,null,0.,0.,1.); csec.StopSoundSequence(CHAN_VOICE); + cse = csec.ceilingdata; } } } diff --git a/zscript/utility/swwm_utility.zsc b/zscript/utility/swwm_utility.zsc index 45e8b4065..b02282deb 100644 --- a/zscript/utility/swwm_utility.zsc +++ b/zscript/utility/swwm_utility.zsc @@ -1670,6 +1670,62 @@ Class SWWMUtility return false, checkme; } + // iterate through polyobjects and see if this line is part of one (returning which, if any) + static bool IsPolyLine( Line l, out swwm_PolyobjectHandle o ) + { + let pi = swwm_PolyobjectIterator.Create(); + swwm_PolyobjectHandle p; + while ( p = pi.Next() ) + { + if ( p.Lines.Find(l) >= p.Lines.Size() ) continue; + o = p; + return true; + } + o = null; + return false; + } + + // checks if the specified world coordinate is inside the polyobject + // this check is very naive but it should handle most "normal" shapes + // (yeah, sorry if you somehow want to play this mod with lilith.pk3) + static bool PointInPolyobj( Vector2 p, swwm_PolyobjectHandle o ) + { + // first pass, find which vertex out of all lines is closest + Vertex v = o.StartLine.v1; + double dist = (v.p-p).length(); + for ( int i=0; i acchits; int hitplane; @@ -54,6 +60,7 @@ Class BusterWall : Thinker double cutheight; // cached Vector3 boundsmin, boundsmax, step; + Array polygrid; override void Tick() { @@ -84,6 +91,11 @@ Class BusterWall : Thinker private void SpawnDebris( bool initial = false ) { + if ( hitpoly ) + { + SpawnDebrisPoly(initial); + return; + } double x, y, z; for ( z=boundsmin.z; z (bustmax/2)) ) continue; + int numpt = Random[Wallbuster](-4,1); + for ( int i=0; i>4)); + bust.accdamage += accdamage; + bust.acchits.Push(accdamage); + bust.bustdir = (bust.bustdir+x)*.5; + // skip if already busted + if ( bust.busted ) return true; + // not enough total damage + if ( bust.accdamage < 100 ) return false; + // estimate polyobject volume + Vector3 a = (32767,32767,32767), b = (-32768,-32768,-32768); + for ( int i=0; i b.x ) b.x = l.v1.p.x; + if ( l.v2.p.x > b.x ) b.x = l.v2.p.x; + if ( l.v1.p.y > b.y ) b.y = l.v1.p.y; + if ( l.v2.p.y > b.y ) b.y = l.v2.p.y; + Sector s = level.PointInSector(l.v1.p); + double fz = s.floorplane.ZAtPoint(l.v1.p); + double cz = s.ceilingplane.ZAtPoint(l.v1.p); + if ( fz < a.z ) a.z = fz; + if ( cz > b.z ) b.z = cz; + s = level.PointInSector(l.v2.p); + fz = s.floorplane.ZAtPoint(l.v2.p); + cz = s.ceilingplane.ZAtPoint(l.v2.p); + if ( fz < a.z ) a.z = fz; + if ( cz > b.z ) b.z = cz; + } + double girthitude = (b.x-a.x)*(b.y-a.y)*(b.z-a.z); + // do a grid check to approximate "real" volume + double ystep = (b.y-a.y)/64.; + double xstep = (b.x-a.x)/64.; + int inspot = 0, allspot = 0; + for ( double y=a.y; y<=b.y; y+=ystep ) for ( double x=a.x; x<=b.x; x+=xstep ) + { + allspot++; + if ( !SWWMUtility.PointInPolyobj((x,y),p) ) continue; + inspot++; + } + if ( allspot <= 0 ) return false; // what the fuck? + girthitude = (girthitude*inspot)/allspot; + // too fucking huge + if ( (girthitude > 16777216) || (max(b.z-a.z,max(b.x-a.x,b.y-a.y)) > 1024) ) return false; + // not strong enough to bust + if ( bust.accdamage < girthitude/300. ) return false; + // report bust + if ( Instigator && Instigator.player ) + { + let s = SWWMStats.Find(Instigator.player); + if ( s ) s.busts++; + SWWMUtility.AchievementProgressInc('swwm_progress_bustin',1,Instigator.player); + } + // call hit fx for devastation sigil (if any) + AngeryPower as = instigator?AngeryPower(instigator.FindInventory("AngeryPower")):null; + if ( as ) as.DoHitFX(); + bust.busted = true; + bust.busttics = 0; + bust.bustmax = min(30,int(12+girthitude**.1)); + // quakin' + let q = Actor.Spawn("BustedQuake",(p.LastPos.x,p.LastPos.y,(b.z+a.z)/2)); + q.special1 = clamp(int(girthitude**.15),1,9); + // "precache" the grid for busting effects + bust.boundsmin = a; + bust.boundsmax = b; + bust.step = (clamp((b.x-a.x)/4.,2.,32.),clamp((b.y-a.y)/4.,2.,32.),clamp((b.z-a.z)/4.,2.,32.)); + for ( double y=a.y; y<=b.y; y+=bust.step.y ) for ( double x=a.x; x<=b.x; x+=bust.step.x ) + { + if ( !SWWMUtility.PointInPolyobj((x,y),p) ) continue; + let g = new("BustPoint"); + g.pos = (x,y); + bust.polygrid.Push(g); + } + // stop any polyobject movement + level.ExecuteSpecial(Polyobj_Stop,instigator,p.StartLine,Line.Front,p.PolyobjectNum); + if ( p.Mirror ) level.ExecuteSpecial(Polyobj_Stop,instigator,p.Mirror.StartLine,Line.Front,p.Mirror.PolyobjectNum); + // send it to the shadow realm (and ensure it stays there) + if ( !p.FindEffector("SWWMBustedPolyobj") ) + { + let yeet = new("SWWMBustedPolyobj"); + yeet.whomstdve = instigator; + p.AddEffector(yeet); + } + bust.SpawnDebris(true); + // damnums + Vector3 bcenter = (bust.boundsmin+bust.boundsmax)*.5; + if ( swwm_accdamage ) + SWWMScoreObj.Spawn(-bust.accdamage,level.Vec3Offset(bcenter,(FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8))),ST_Damage); + else for ( int i=0; i>4)); bust.accdamage += accdamage; bust.acchits.Push(accdamage); @@ -353,6 +524,9 @@ Class BusterWall : Thinker if ( s ) s.busts++; SWWMUtility.AchievementProgressInc("bustin",1,Instigator.player); } + // call hit fx for devastation sigil (if any) + AngeryPower as = instigator?AngeryPower(instigator.FindInventory("AngeryPower")):null; + if ( as ) as.DoHitFX(); bust.busted = true; bust.busttics = 0; bust.bustmax = min(30,int(12+girthitude**.1)); @@ -374,39 +548,37 @@ Class BusterWall : Thinker if ( !l.sidedef[1] ) continue; int away = 0; if ( l.sidedef[0].sector == hs ) away = 1; - // temporarily set filler texture so switches don't play a sound - TextureID oldtex[3][2]; - for ( int j=0; j<3; j++ ) for ( int k=0; k<2; k++ ) - { - oldtex[j][k] = l.sidedef[k].GetTexture(j); - l.sidedef[k].SetTexture(j,rubble); - } l.Activate(instigator,away,SPAC_Use); l.Activate(instigator,away,SPAC_Impact); - for ( int j=0; j<3; j++ ) for ( int k=0; k<2; k++ ) - l.sidedef[k].SetTexture(j,oldtex[j][k]); - // clear any use/impact specials - if ( l.Activation&(SPAC_Use|SPAC_Impact) ) - l.Activation &= ~(SPAC_Use|SPAC_Impact); } - // stop movement permanently - SWWMCrusherBroken.Create(hp?null:hs,hp?hs:null,0.,true); + // if this is a broken crusher, we need to clear that now that we've busted it + SWWMCrusherBroken.Remove(hp?null:hs,hp?hs:null); // quakin' let q = Actor.Spawn("BustedQuake",(hs.centerspot.x,hs.centerspot.y,thisheight)); q.special1 = clamp(int(girthitude**.15),1,9); if ( hp ) { + // remove any current movers + if ( hs.CeilingData ) hs.CeilingData.Destroy(); // blow up that ceiling hs.MoveCeiling(abs(partheight),bust.cutheight,0,1,false); bust.boundsmin = (a.x,a.y,thisheight)+(1,1,1); bust.boundsmax = (b.x,b.y,bust.cutheight)-(1,1,1); + // prevent any further ceiling movement + level.CreateCeiling(hs,Ceiling.ceilRaiseByValue,null,0.,0.,1.); + hs.StopSoundSequence(CHAN_VOICE); } else { + // remove any current movers + if ( hs.FloorData ) hs.FloorData.Destroy(); // blow up that floor hs.MoveFloor(abs(partheight),abs(bust.cutheight),0,-1,false,true); bust.boundsmin = (a.x,a.y,bust.cutheight)+(1,1,1); bust.boundsmax = (b.x,b.y,thisheight)-(1,1,1); + // prevent any further floor movement + level.CreateFloor(hs,Floor.floorLowerByValue,null,0.,1.); + hs.StopSoundSequence(CHAN_WEAPON); } bust.step = (clamp((b.x-a.x)/4.,2.,32.),clamp((b.y-a.y)/4.,2.,32.),clamp(partheight/4.,2.,32.)); bust.SpawnDebris(true);