diff --git a/specs/udmf_zdoom.txt b/specs/udmf_zdoom.txt index 84dcd4d29..e26044dc6 100644 --- a/specs/udmf_zdoom.txt +++ b/specs/udmf_zdoom.txt @@ -141,6 +141,11 @@ Note: All fields default to false unless mentioned otherwise. // 13: Portal line. revealed = ; // true = line is initially visible on automap. + health = ; // Amount of hitpoints for this line. + healthgroup = ; // ID of destructible object to synchronize hitpoints (optional, default is 0) + damagespecial = ; // This line will call special if having health>0 and receiving damage + deathspecial = ; // This line will call special if health was reduced to 0 + * Note about arg0str For lines with ACS specials (80-86 and 226), if arg0str is present and non-null, it @@ -248,17 +253,21 @@ Note: All fields default to false unless mentioned otherwise. color_sprites = ; // Material color of sprites in sector. Default is white (0xffffff) - portal_ceil_blocksound = // ceiling portal blocks sound. - portal_ceil_disabled = // ceiling portal disabled. - portal_ceil_nopass = // ceiling portal blocks movement if true. - portal_ceil_norender = // ceiling portal not rendered. - portal_ceil_overlaytype = // defines translucency style, can either be "translucent" or "additive". Default is "translucent". - portal_floor_blocksound = // floor portal blocks sound. - portal_floor_disabled = // floor portal disabled. - portal_floor_nopass = // ceiling portal blocks movement if true. - portal_floor_norender = // ceiling portal not rendered. - portal_floor_overlaytype = // defines translucency style, can either be "translucent" or "additive". Default is "translucent". + portal_ceil_blocksound = ; // ceiling portal blocks sound. + portal_ceil_disabled = ; // ceiling portal disabled. + portal_ceil_nopass = ; // ceiling portal blocks movement if true. + portal_ceil_norender = ; // ceiling portal not rendered. + portal_ceil_overlaytype = ; // defines translucency style, can either be "translucent" or "additive". Default is "translucent". + portal_floor_blocksound = ; // floor portal blocks sound. + portal_floor_disabled = ; // floor portal disabled. + portal_floor_nopass = ; // ceiling portal blocks movement if true. + portal_floor_norender = ; // ceiling portal not rendered. + portal_floor_overlaytype = ; // defines translucency style, can either be "translucent" or "additive". Default is "translucent". + healthfloor = ; // Amount of hitpoints for this sector (includes floor and bottom-outside linedef sides) + healthfloorgroup = ; // ID of destructible object to synchronize hitpoints (optional, default is 0) + healthceiling = ; // Amount of hitpoints for this sector (includes ceiling and top-outside linedef sides) + healthceilinggroup = ; // ID of destructible object to synchronize hitpoints (optional, default is 0) * Note about dropactors diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8a055bda2..d5ebda5b1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -948,6 +948,7 @@ set (PCH_SOURCES p_actionfunctions.cpp p_ceiling.cpp p_conversation.cpp + p_destructible.cpp p_doors.cpp p_effect.cpp p_enemy.cpp diff --git a/src/actionspecials.h b/src/actionspecials.h index 7d1ac1d98..d62f93536 100644 --- a/src/actionspecials.h +++ b/src/actionspecials.h @@ -152,6 +152,8 @@ DEFINE_SPECIAL(Sector_Set3DFloor, 160, -1, -1, 5) DEFINE_SPECIAL(Sector_SetContents, 161, -1, -1, 3) // [RH] Begin new specials for ZDoom +DEFINE_SPECIAL(Line_SetHealth, 150, 2, 2, 2) +DEFINE_SPECIAL(Sector_SetHealth, 151, 3, 3, 3) DEFINE_SPECIAL(Ceiling_CrushAndRaiseDist, 168, 3, 5, 5) DEFINE_SPECIAL(Generic_Crusher2, 169, 5, 5, 5) DEFINE_SPECIAL(Sector_SetCeilingScale2, 170, 3, 3, 3) diff --git a/src/doomdata.h b/src/doomdata.h index eb54f5f32..ee0b21f32 100644 --- a/src/doomdata.h +++ b/src/doomdata.h @@ -191,6 +191,8 @@ enum SPAC SPAC_MUse = 1<<8, // monsters can use SPAC_MPush = 1<<9, // monsters can push SPAC_UseBack = 1<<10, // Can be used from the backside + SPAC_Damage = 1<<11, // [ZZ] when linedef receives damage + SPAC_Death = 1<<12, // [ZZ] when linedef receives damage and has 0 health SPAC_PlayerActivate = (SPAC_Cross|SPAC_Use|SPAC_Impact|SPAC_Push|SPAC_AnyCross|SPAC_UseThrough|SPAC_UseBack), }; diff --git a/src/g_levellocals.h b/src/g_levellocals.h index 28d0c16df..dfdb6dbef 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -40,6 +40,8 @@ #include "r_defs.h" #include "portal.h" #include "p_blockmap.h" +#include "p_local.h" +#include "p_destructible.h" struct FLevelLocals { @@ -96,6 +98,9 @@ struct FLevelLocals TArray Zones; + // [ZZ] Destructible geometry information + TMap healthGroups; + FBlockmap blockmap; // These are copies of the loaded map data that get used by the savegame code to skip unaltered fields diff --git a/src/namedef.h b/src/namedef.h index b098a06c0..c66890551 100644 --- a/src/namedef.h +++ b/src/namedef.h @@ -668,6 +668,14 @@ xx(Prev) xx(Children) xx(Owner) +xx(HealthFloor) +xx(HealthCeiling) +xx(DamageSpecial) +xx(DeathSpecial) +xx(HealthFloorGroup) +xx(HealthCeilingGroup) +xx(HealthGroup) + // USDF keywords xx(Amount) xx(Text) diff --git a/src/p_acs.cpp b/src/p_acs.cpp index 445c8125f..712cddf1b 100644 --- a/src/p_acs.cpp +++ b/src/p_acs.cpp @@ -4975,6 +4975,8 @@ enum EACSFunctions ACSF_Ceil, ACSF_ScriptCall, ACSF_StartSlideshow, + ACSF_GetSectorHealth, + ACSF_GetLineHealth, // Eternity's ACSF_GetLineX = 300, @@ -6841,6 +6843,44 @@ doplaysound: if (funcIndex == ACSF_PlayActorSound) G_StartSlideshow(FName(FBehavior::StaticLookupString(args[0]))); break; + case ACSF_GetSectorHealth: + { + int part = args[1]; + FSectorTagIterator it(args[0]); + int s = it.Next(); + if (s < 0) + return 0; + sector_t* ss = &level.sectors[s]; + FHealthGroup* grp; + if (part == SECPART_Ceiling) + { + return (ss->healthceilinggroup && (grp = P_GetHealthGroup(ss->healthceilinggroup))) + ? grp->health : ss->healthceiling; + } + else if (part == SECPART_Floor) + { + return (ss->healthfloorgroup && (grp = P_GetHealthGroup(ss->healthfloorgroup))) + ? grp->health : ss->healthfloor; + } + return 0; + } + + case ACSF_GetLineHealth: + { + FLineIdIterator it(args[0]); + int l = it.Next(); + if (l < 0) + return 0; + line_t* ll = &level.lines[l]; + if (ll->healthgroup > 0) + { + FHealthGroup* grp = P_GetHealthGroup(ll->healthgroup); + if (grp) return grp->health; + } + + return ll->health; + } + case ACSF_GetLineX: case ACSF_GetLineY: { diff --git a/src/p_destructible.cpp b/src/p_destructible.cpp new file mode 100755 index 000000000..3cb00d5f9 --- /dev/null +++ b/src/p_destructible.cpp @@ -0,0 +1,532 @@ +#include "p_spec.h" +#include "g_levellocals.h" +#include "p_destructible.h" +#include "v_text.h" +#include "actor.h" +#include "p_trace.h" +#include "p_lnspec.h" +#include "r_sky.h" +#include "p_local.h" +#include "p_maputl.h" +#include "c_cvars.h" + +//========================================================================== +// +// [ZZ] Geometry damage logic callbacks +// +//========================================================================== +void P_SetHealthGroupHealth(int group, int health) +{ + FHealthGroup* grp = P_GetHealthGroup(group); + if (!grp) return; + + grp->health = health; + + // now set health of all linked objects to the same as this one + for (unsigned i = 0; i < grp->lines.Size(); i++) + { + line_t* lline = grp->lines[i]; + lline->health = health; + } + // + for (unsigned i = 0; i < grp->sectors.Size(); i++) + { + sector_t* lsector = grp->sectors[i]; + if (lsector->healthceilinggroup == group) + lsector->healthceiling = health; + if (lsector->healthfloorgroup == group) + lsector->healthfloor = health; + } +} + +void P_DamageHealthGroup(FHealthGroup* grp, void* object, AActor* source, int damage, FName damagetype, int side, int part, DVector3 position) +{ + if (!grp) return; + int group = grp->id; + + // now set health of all linked objects to the same as this one + for (unsigned i = 0; i < grp->lines.Size(); i++) + { + line_t* lline = grp->lines[i]; + if (lline == object) + continue; + lline->health = grp->health + damage; + P_DamageLinedef(lline, source, damage, damagetype, side, position, false); + } + // + for (unsigned i = 0; i < grp->sectors.Size(); i++) + { + sector_t* lsector = grp->sectors[i]; + + if (lsector->healthceilinggroup == group && (lsector != object || part != SECPART_Ceiling)) + { + lsector->healthceiling = grp->health + damage; + P_DamageSector(lsector, source, damage, damagetype, SECPART_Ceiling, position, false); + } + + if (lsector->healthfloorgroup == group && (lsector != object || part != SECPART_Floor)) + { + lsector->healthfloor = grp->health + damage; + P_DamageSector(lsector, source, damage, damagetype, SECPART_Floor, position, false); + } + } +} + +void P_DamageLinedef(line_t* line, AActor* source, int damage, FName damagetype, int side, DVector3 position, bool dogroups) +{ + line->health -= damage; + if (line->health < 0) line->health = 0; + + // callbacks here + // first off, call special if needed + if ((line->activation & SPAC_Damage) || ((line->activation & SPAC_Death) && !line->health)) + { + int activateon = SPAC_Damage; + if (!line->health) activateon |= SPAC_Death; + P_ActivateLine(line, source, side, activateon&line->activation, &position); + } + + if (dogroups && line->healthgroup) + { + FHealthGroup* grp = P_GetHealthGroup(line->healthgroup); + if (grp) + grp->health = line->health; + P_DamageHealthGroup(grp, line, source, damage, damagetype, side, -1, position); + } + + //Printf("P_DamageLinedef: %d damage (type=%s, source=%p), new health = %d\n", damage, damagetype.GetChars(), source, line->health); +} + +void P_DamageSector(sector_t* sector, AActor* source, int damage, FName damagetype, int part, DVector3 position, bool dogroups) +{ + int sectorhealth = (part == SECPART_Ceiling) ? sector->healthceiling : sector->healthfloor; + int newhealth = sectorhealth - damage; + if (newhealth < 0) newhealth = 0; + if (part == SECPART_Ceiling) + sector->healthceiling = newhealth; + else sector->healthfloor = newhealth; + + // callbacks here + int dmg = (part == SECPART_Ceiling) ? SECSPAC_DamageCeiling : SECSPAC_DamageFloor; + int dth = (part == SECPART_Ceiling) ? SECSPAC_DeathCeiling : SECSPAC_DeathFloor; + if (sector->SecActTarget) + { + sector->TriggerSectorActions(source, dmg); + if (newhealth == 0) + sector->TriggerSectorActions(source, dth); + } + + int group = (part == SECPART_Ceiling) ? sector->healthceilinggroup : sector->healthfloorgroup; + if (dogroups && group) + { + FHealthGroup* grp = P_GetHealthGroup(group); + if (grp) + grp->health = newhealth; + P_DamageHealthGroup(grp, sector, source, damage, damagetype, 0, part, position); + } + + //Printf("P_DamageSector: %d damage (type=%s, position=%s, source=%p), new health = %d\n", damage, damagetype.GetChars(), (part == SECPART_Ceiling) ? "ceiling" : "floor", source, newhealth); +} + + +//=========================================================================== +// +// +// +//=========================================================================== + +FHealthGroup* P_GetHealthGroup(int id) +{ + FHealthGroup* grp = level.healthGroups.CheckKey(id); + return grp; +} + +FHealthGroup* P_GetHealthGroupOrNew(int id, int health) +{ + FHealthGroup* grp = level.healthGroups.CheckKey(id); + if (!grp) + { + FHealthGroup newgrp; + newgrp.id = id; + newgrp.health = health; + grp = &(level.healthGroups.Insert(id, newgrp)); + } + return grp; +} + +void P_InitHealthGroups() +{ + level.healthGroups.Clear(); + TArray groupsInError; + for (unsigned i = 0; i < level.lines.Size(); i++) + { + line_t* lline = &level.lines[i]; + if (lline->healthgroup > 0) + { + FHealthGroup* grp = P_GetHealthGroupOrNew(lline->healthgroup, lline->health); + if (grp->health != lline->health) + { + Printf(TEXTCOLOR_RED "Warning: line %d in health group %d has different starting health (line health = %d, group health = %d)\n", i, lline->healthgroup, lline->health, grp->health); + groupsInError.Push(lline->healthgroup); + if (lline->health > grp->health) + grp->health = lline->health; + } + grp->lines.Push(lline); + } + } + for (unsigned i = 0; i < level.sectors.Size(); i++) + { + sector_t* lsector = &level.sectors[i]; + if (lsector->healthceilinggroup > 0) + { + FHealthGroup* grp = P_GetHealthGroupOrNew(lsector->healthceilinggroup, lsector->healthceiling); + if (grp->health != lsector->healthceiling) + { + Printf(TEXTCOLOR_RED "Warning: sector ceiling %d in health group %d has different starting health (ceiling health = %d, group health = %d)\n", i, lsector->healthceilinggroup, lsector->healthceiling, grp->health); + groupsInError.Push(lsector->healthceilinggroup); + if (lsector->healthceiling > grp->health) + grp->health = lsector->healthceiling; + } + grp->sectors.Push(lsector); + } + if (lsector->healthfloorgroup > 0) + { + FHealthGroup* grp = P_GetHealthGroupOrNew(lsector->healthfloorgroup, lsector->healthfloor); + if (grp->health != lsector->healthfloor) + { + Printf(TEXTCOLOR_RED "Warning: sector floor %d in health group %d has different starting health (floor health = %d, group health = %d)\n", i, lsector->healthfloorgroup, lsector->healthfloor, grp->health); + groupsInError.Push(lsector->healthfloorgroup); + if (lsector->healthfloor > grp->health) + grp->health = lsector->healthfloor; + } + if (lsector->healthceilinggroup != lsector->healthfloorgroup) + grp->sectors.Push(lsector); + } + } + for (unsigned i = 0; i < groupsInError.Size(); i++) + { + FHealthGroup* grp = P_GetHealthGroup(groupsInError[i]); + Printf(TEXTCOLOR_GOLD "Health group %d is using the highest found health value of %d", groupsInError[i], grp->health); + } +} + +//========================================================================== +// +// P_GeometryLineAttack +// +// Applies hitscan damage to geometry (lines and sectors with health>0) +//========================================================================== + +void P_GeometryLineAttack(FTraceResults& trace, AActor* thing, int damage, FName damageType) +{ + // [ZZ] hitscan geometry damage logic + // + if (trace.HitType == TRACE_HitWall && P_CheckLinedefVulnerable(trace.Line, trace.Side)) + { + if (trace.Tier == TIER_Lower || trace.Tier == TIER_Upper) // process back sector health if any + { + sector_t* backsector = (trace.Line->sidedef[!trace.Side] ? trace.Line->sidedef[!trace.Side]->sector : nullptr); + int sectorhealth = 0; + if (backsector && trace.Tier == TIER_Lower && backsector->healthfloor > 0 && P_CheckLinedefVulnerable(trace.Line, trace.Side, SECPART_Floor)) + sectorhealth = backsector->healthfloor; + if (backsector && trace.Tier == TIER_Upper && backsector->healthceiling > 0 && P_CheckLinedefVulnerable(trace.Line, trace.Side, SECPART_Ceiling)) + sectorhealth = backsector->healthceiling; + if (sectorhealth > 0) + { + P_DamageSector(backsector, thing, damage, damageType, (trace.Tier == TIER_Upper) ? SECPART_Ceiling : SECPART_Floor, trace.HitPos); + } + } + // always process linedef health if any + if (trace.Line->health > 0) + { + P_DamageLinedef(trace.Line, thing, damage, damageType, trace.Side, trace.HitPos); + } + // fake floors are not handled + } + else if (trace.HitType == TRACE_HitFloor || trace.HitType == TRACE_HitCeiling) + { + int sectorhealth = 0; + if (trace.HitType == TRACE_HitFloor && trace.Sector->healthfloor > 0 && P_CheckSectorVulnerable(trace.Sector, SECPART_Floor)) + sectorhealth = trace.Sector->healthfloor; + if (trace.HitType == TRACE_HitCeiling && trace.Sector->healthceiling > 0 && P_CheckSectorVulnerable(trace.Sector, SECPART_Ceiling)) + sectorhealth = trace.Sector->healthceiling; + if (sectorhealth > 0) + { + P_DamageSector(trace.Sector, thing, damage, damageType, (trace.HitType == TRACE_HitCeiling) ? SECPART_Ceiling : SECPART_Floor, trace.HitPos); + } + } +} + +//========================================================================== +// +// P_GeometryRadiusAttack +// +// Applies radius damage to destructible geometry (lines and sectors with health>0) +//========================================================================== + +struct pgra_data_t +{ + DVector3 pos; + line_t* line; + sector_t* sector; + int secpart; +}; + +// we use this horizontally for 2D wall distance, and in a bit fancier way for 3D wall distance +static DVector2 PGRA_ClosestPointOnLine2D(DVector2 x, DVector2 p1, DVector2 p2) +{ + DVector2 p2p1 = (p2 - p1); + DVector2 xp1 = (x - p1); + double r = p2p1 | xp1; + double len = p2p1.Length(); + r /= len; + + if (r < 0) + return p1; + if (r > len) + return p2; + + return p1 + p2p1.Unit() * r; +} + +static void PGRA_InsertIfCloser(TMap& damageGroupPos, int group, DVector3 pt, DVector3 check, sector_t* checksector, sector_t* sector, line_t* line, int secpart) +{ + // simple solid geometry sight check between "check" and "pt" + // expected - Trace hits nothing + DVector3 ptVec = (pt - check); + double ptDst = ptVec.Length() - 0.5; + ptVec.MakeUnit(); + FTraceResults res; + bool isblocked = Trace(check, checksector, ptVec, ptDst, 0, 0xFFFFFFFF, nullptr, res); + if (isblocked) return; + + pgra_data_t* existing = damageGroupPos.CheckKey(group); + // not present or distance is closer + if (!existing || (((*existing).pos - check).Length() > (pt - check).Length())) + { + if (!existing) existing = &damageGroupPos.Insert(group, pgra_data_t()); + existing->pos = pt; + existing->line = line; + existing->sector = sector; + existing->secpart = secpart; + } +} + +EXTERN_CVAR(Float, splashfactor); + +void P_GeometryRadiusAttack(AActor* bombspot, AActor* bombsource, int bombdamage, int bombdistance, FName damagetype, int fulldamagedistance) +{ + TMap damageGroupPos; + + double bombdistancefloat = 1. / (double)(bombdistance - fulldamagedistance); + + // now, this is not entirely correct... but sector actions still _do_ require a valid source actor to trigger anything + if (!bombspot) + return; + if (!bombsource) + bombsource = bombspot; + + // check current sector floor and ceiling. this is the only *sector* checked, otherwise only use lines + // a bit imprecise, but should work + sector_t* srcsector = bombspot->Sector; + + if (srcsector->healthceiling > 0) + { + double dstceiling = srcsector->ceilingplane.Normal() | (bombspot->Pos() + srcsector->ceilingplane.Normal()*srcsector->ceilingplane.D); + DVector3 spotTo = bombspot->Pos() - srcsector->ceilingplane.Normal() * dstceiling; + int grp = srcsector->healthceilinggroup; + if (grp <= 0) + grp = 0x80000000 | (srcsector->sectornum & 0x7FFFFFFF); + PGRA_InsertIfCloser(damageGroupPos, grp, spotTo, bombspot->Pos(), srcsector, srcsector, nullptr, SECPART_Ceiling); + } + + if (srcsector->healthfloor > 0) + { + double dstfloor = srcsector->floorplane.Normal() | (bombspot->Pos() + srcsector->floorplane.Normal()*srcsector->floorplane.D); + DVector3 spotTo = bombspot->Pos() - srcsector->floorplane.Normal() * dstfloor; + int grp = srcsector->healthfloorgroup; + if (grp <= 0) + grp = 0x40000000 | (srcsector->sectornum & 0x7FFFFFFF); + PGRA_InsertIfCloser(damageGroupPos, grp, spotTo, bombspot->Pos(), srcsector, srcsector, nullptr, SECPART_Floor); + } + + // enumerate all lines around + FBoundingBox bombbox(bombspot->X(), bombspot->Y(), bombdistance * 16); + FBlockLinesIterator it(bombbox); + line_t* ln; + int vc = validcount; + TArray lines; + while ((ln = it.Next())) // iterator and Trace both use validcount and interfere with each other + lines.Push(ln); + for (int i = 0; i < lines.Size(); i++) + { + ln = lines[i]; + DVector2 pos2d = bombspot->Pos().XY(); + int sd = P_PointOnLineSide(pos2d, ln); + + side_t* side = ln->sidedef[sd]; + if (!side) continue; + sector_t* sector = side->sector; + side_t* otherside = ln->sidedef[!sd]; + sector_t* othersector = otherside ? otherside->sector : nullptr; + if (!ln->health && (!othersector || (!othersector->healthfloor && !othersector->healthceiling))) + continue; // non-interactive geometry + + DVector2 to2d = PGRA_ClosestPointOnLine2D(bombspot->Pos().XY(), side->V1()->p, side->V2()->p); + // this gives us x/y + double distto2d = (to2d - pos2d).Length(); + double z_top1, z_top2, z_bottom1, z_bottom2; // here, z_top1 is closest to the ceiling, and z_bottom1 is closest to the floor. + z_top1 = sector->ceilingplane.ZatPoint(to2d); + z_bottom1 = sector->floorplane.ZatPoint(to2d); + DVector3 to3d_fullheight(to2d.X, to2d.Y, clamp(bombspot->Z(), z_bottom1, z_top1)); + + if (ln->health && P_CheckLinedefVulnerable(ln, sd)) + { + bool cantdamage = false; + bool linefullheight = othersector && !!(ln->flags & (ML_BLOCKEVERYTHING)); + // decide specific position to affect on a line. + if (!linefullheight) + { + z_top2 = othersector->ceilingplane.ZatPoint(to2d); + z_bottom2 = othersector->floorplane.ZatPoint(to2d); + double bombz = bombspot->Z(); + if (z_top2 > z_top1) + z_top2 = z_top1; + if (z_bottom2 < z_bottom1) + z_bottom2 = z_bottom1; + if (bombz <= z_top2 && bombz >= z_bottom2) // between top and bottom opening + to3d_fullheight.Z = (z_top2 - bombz < bombz - z_bottom2) ? z_top2 : z_bottom2; + else if (bombz < z_bottom2 && bombz >= z_bottom1) + to3d_fullheight.Z = clamp(bombz, z_bottom1, z_bottom2); + else if (bombz > z_top2 && bombz <= z_top1) + to3d_fullheight.Z = clamp(bombz, z_top2, z_top1); + else cantdamage = true; + } + + if (!cantdamage) + { + if (ln->healthgroup) + { + PGRA_InsertIfCloser(damageGroupPos, ln->healthgroup, to3d_fullheight, bombspot->Pos(), srcsector, nullptr, ln, -1); + } + else + { + // otherwise just damage line + double dst = (to3d_fullheight - bombspot->Pos()).Length(); + int damage = 0; + if (dst < bombdistance) + { + dst = clamp(dst - (double)fulldamagedistance, 0, dst); + damage = (int)((double)bombdamage * (1. - dst * bombdistancefloat)); + if (bombsource == bombspot) + damage = (int)(damage * splashfactor); + } + P_DamageLinedef(ln, bombsource, damage, damagetype, sd, to3d_fullheight); + } + } + } + + if (othersector && othersector->healthceiling && P_CheckLinedefVulnerable(ln, sd, SECPART_Ceiling)) + { + z_top2 = othersector->ceilingplane.ZatPoint(to2d); + if (z_top2 < z_top1) // we have front side to hit against + { + DVector3 to3d_upper(to2d.X, to2d.Y, clamp(bombspot->Z(), z_top2, z_top1)); + int grp = othersector->healthceilinggroup; + if (grp <= 0) + grp = 0x80000000 | (othersector->sectornum & 0x7FFFFFFF); + PGRA_InsertIfCloser(damageGroupPos, grp, to3d_upper, bombspot->Pos(), srcsector, othersector, nullptr, SECPART_Ceiling); + } + } + + if (othersector && othersector->healthfloor && P_CheckLinedefVulnerable(ln, sd, SECPART_Floor)) + { + z_bottom2 = othersector->floorplane.ZatPoint(to2d); + if (z_bottom2 > z_bottom1) // we have front side to hit against + { + DVector3 to3d_lower(to2d.X, to2d.Y, clamp(bombspot->Z(), z_bottom1, z_bottom2)); + int grp = othersector->healthfloorgroup; + if (grp <= 0) + grp = 0x40000000 | (othersector->sectornum & 0x7FFFFFFF); + PGRA_InsertIfCloser(damageGroupPos, grp, to3d_lower, bombspot->Pos(), srcsector, othersector, nullptr, SECPART_Floor); + } + } + } + + // damage health groups and sectors. + // if group & 0x80000000, this is sector ceiling without health group + // if grpup & 0x40000000, this is sector floor without health group + // otherwise this is some object in health group + TMap::ConstIterator damageGroupIt(damageGroupPos); + TMap::ConstPair* damageGroupPair; + while (damageGroupIt.NextPair(damageGroupPair)) + { + DVector3 pos = damageGroupPair->Value.pos; + double dst = (pos - bombspot->Pos()).Length(); + int damage = 0; + if (dst < bombdistance) + { + dst = clamp(dst - (double)fulldamagedistance, 0, dst); + damage = (int)((double)bombdamage * (1. - dst * bombdistancefloat)); + if (bombsource == bombspot) + damage = (int)(damage * splashfactor); + } + + // for debug + //P_SpawnParticle(damageGroupPair->Value.pos, DVector3(), DVector3(), PalEntry(0xFF, 0x00, 0x00), 1.0, 35, 5.0, 0.0, 0.0, 0); + + int grp = damageGroupPair->Key; + if (grp & 0x80000000) // sector ceiling + { + assert(damageGroupPair->Value.sector != nullptr); + P_DamageSector(damageGroupPair->Value.sector, bombsource, damage, damagetype, SECPART_Ceiling, pos); + } + else if (grp & 0x40000000) // sector floor + { + assert(damageGroupPair->Value.sector != nullptr); + P_DamageSector(damageGroupPair->Value.sector, bombsource, damage, damagetype, SECPART_Floor, pos); + } + else + { + assert((damageGroupPair->Value.sector != nullptr) != (damageGroupPair->Value.line != nullptr)); + if (damageGroupPair->Value.line != nullptr) + P_DamageLinedef(damageGroupPair->Value.line, bombsource, damage, damagetype, P_PointOnLineSide(pos.XY(), damageGroupPair->Value.line), pos); + else P_DamageSector(damageGroupPair->Value.sector, bombsource, damage, damagetype, damageGroupPair->Value.secpart, pos); + } + } +} + +//========================================================================== +// +// P_CheckLinedefVulnerable +// +// If sectorpart is <0: returns if linedef is damageable directly +// If sectorpart is valid (SECPART_ enum): returns if sector on sidedef is +// damageable through this line at specified sectorpart +//========================================================================== + +bool P_CheckLinedefVulnerable(line_t* line, int side, int sectorpart) +{ + if (line->special == Line_Horizon) + return false; + side_t* sidedef = line->sidedef[side]; + if (!sidedef) + return false; + return true; +} + +//========================================================================== +// +// P_CheckSectorVulnerable +// +// Returns true if sector's floor or ceiling is directly damageable +//========================================================================== + +bool P_CheckSectorVulnerable(sector_t* sector, int part) +{ + FTextureID texture = sector->GetTexture((part == SECPART_Ceiling) ? sector_t::ceiling : sector_t::floor); + secplane_t* plane = (part == SECPART_Ceiling) ? §or->ceilingplane : §or->floorplane; + if (texture == skyflatnum) + return false; + return true; +} \ No newline at end of file diff --git a/src/p_destructible.h b/src/p_destructible.h new file mode 100755 index 000000000..81b74faa8 --- /dev/null +++ b/src/p_destructible.h @@ -0,0 +1,37 @@ +#pragma once + +#include "tarray.h" +#include "r_defs.h" +#include "p_trace.h" + +// [ZZ] Destructible geometry related +struct FHealthGroup +{ + TArray sectors; + TArray lines; + int health; + int id; +}; + +// for P_DamageSector +enum +{ + SECPART_Floor = 0, + SECPART_Ceiling = 1 +}; + +void P_InitHealthGroups(); + +void P_SetHealthGroupHealth(int group, int health); + +FHealthGroup* P_GetHealthGroup(int id); +FHealthGroup* P_GetHealthGroupOrNew(int id, int startinghealth); + +void P_DamageSector(sector_t* sector, AActor* source, int damage, FName damagetype, int part, DVector3 position, bool dogroups = true); +void P_DamageLinedef(line_t* line, AActor* source, int damage, FName damagetype, int side, DVector3 position, bool dogroups = true); + +void P_GeometryLineAttack(FTraceResults& trace, AActor* thing, int damage, FName damageType); +void P_GeometryRadiusAttack(AActor* bombspot, AActor* bombsource, int bombdamage, int bombdistance, FName damagetype, int fulldamagedistance); + +bool P_CheckLinedefVulnerable(line_t* line, int side, int part = -1); +bool P_CheckSectorVulnerable(sector_t* sector, int part); \ No newline at end of file diff --git a/src/p_lnspec.cpp b/src/p_lnspec.cpp index 253f505e4..081c7f400 100644 --- a/src/p_lnspec.cpp +++ b/src/p_lnspec.cpp @@ -56,6 +56,7 @@ #include "p_spec.h" #include "g_levellocals.h" #include "vm.h" +#include "p_destructible.h" // Remaps EE sector change types to Generic_Floor values. According to the Eternity Wiki: /* @@ -3478,6 +3479,53 @@ FUNC(LS_Sector_SetCeilingGlow) return true; } +FUNC(LS_Line_SetHealth) +// Line_SetHealth(id, health) +{ + FLineIdIterator itr(arg0); + int l; + + if (arg1 < 0) + arg1 = 0; + + while ((l = itr.Next()) >= 0) + { + line_t* line = &level.lines[l]; + line->health = arg1; + if (line->healthgroup) + P_SetHealthGroupHealth(line->healthgroup, arg1); + } + return true; +} + +FUNC(LS_Sector_SetHealth) +// Sector_SetHealth(id, part, health) +{ + FSectorTagIterator itr(arg0); + int s; + + if (arg2 < 0) + arg2 = 0; + + while ((s = itr.Next()) >= 0) + { + sector_t* sector = &level.sectors[s]; + if (arg1 == SECPART_Ceiling) + { + sector->healthceiling = arg2; + if (sector->healthceilinggroup) + P_SetHealthGroupHealth(sector->healthceilinggroup, arg2); + } + else if (arg1 == SECPART_Floor) + { + sector->healthfloor = arg2; + if (sector->healthfloorgroup) + P_SetHealthGroupHealth(sector->healthfloorgroup, arg2); + } + } + return true; +} + static lnSpecFunc LineSpecials[] = { /* 0 */ LS_NOP, @@ -3630,8 +3678,8 @@ static lnSpecFunc LineSpecials[] = /* 147 */ LS_NOP, /* 148 */ LS_NOP, /* 149 */ LS_NOP, - /* 150 */ LS_NOP, - /* 151 */ LS_NOP, + /* 150 */ LS_Line_SetHealth, + /* 151 */ LS_Sector_SetHealth, /* 152 */ LS_NOP, // 152 Team_Score /* 153 */ LS_NOP, // 153 Team_GivePoints /* 154 */ LS_Teleport_NoStop, diff --git a/src/p_local.h b/src/p_local.h index d1a81a992..e553def29 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -248,7 +248,6 @@ AActor *P_RoughMonsterSearch (AActor *mo, int distance, bool onlyseekable=false, // P_MAP // - struct spechit_t { line_t *line; diff --git a/src/p_map.cpp b/src/p_map.cpp index 58c0bb706..0d9120c1b 100644 --- a/src/p_map.cpp +++ b/src/p_map.cpp @@ -4684,6 +4684,8 @@ AActor *P_LineAttack(AActor *t1, DAngle angle, double distance, { if (trace.HitType != TRACE_HitActor) { + P_GeometryLineAttack(trace, t1, damage, damageType); + // position a bit closer for puffs if (nointeract || trace.HitType != TRACE_HitWall || ((trace.Line->special != Line_Horizon) || spawnSky)) { @@ -5517,6 +5519,8 @@ void P_RailAttack(FRailParams *p) } } + P_GeometryLineAttack(trace, p->source, p->damage, damagetype); + // Spawn a decal or puff at the point where the trace ended. if (trace.HitType == TRACE_HitWall) { @@ -6127,6 +6131,8 @@ int P_RadiusAttack(AActor *bombspot, AActor *bombsource, int bombdamage, int bom bombsource = bombspot; } + P_GeometryRadiusAttack(bombspot, bombsource, bombdamage, bombdistance, bombmod, fulldamagedistance); + int count = 0; while ((it.Next(&cres))) { diff --git a/src/p_mobj.cpp b/src/p_mobj.cpp index bd8da579f..25f77fc79 100644 --- a/src/p_mobj.cpp +++ b/src/p_mobj.cpp @@ -1938,6 +1938,31 @@ bool AActor::Massacre () void P_ExplodeMissile (AActor *mo, line_t *line, AActor *target, bool onsky) { + // [ZZ] line damage callback + if (line) + { + int wside = P_PointOnLineSide(mo->Pos(), line); + int oside = !wside; + side_t* otherside = line->sidedef[oside]; + // check if hit upper or lower part + if (otherside) + { + sector_t* othersector = otherside->sector; + double otherfloorz = othersector->floorplane.ZatPoint(mo->Pos()); + double otherceilingz = othersector->ceilingplane.ZatPoint(mo->Pos()); + double actualz = mo->Pos().Z; + if (actualz < otherfloorz && othersector->healthfloor > 0 && P_CheckLinedefVulnerable(line, wside, SECPART_Floor)) + P_DamageSector(othersector, mo, mo->GetMissileDamage((mo->flags4 & MF4_STRIFEDAMAGE) ? 3 : 7, 1), mo->DamageType, SECPART_Floor, mo->Pos()); + if (actualz > otherceilingz && othersector->healthceiling > 0 && P_CheckLinedefVulnerable(line, wside, SECPART_Ceiling)) + P_DamageSector(othersector, mo, mo->GetMissileDamage((mo->flags4 & MF4_STRIFEDAMAGE) ? 3 : 7, 1), mo->DamageType, SECPART_Ceiling, mo->Pos()); + } + + if (line->health > 0 && P_CheckLinedefVulnerable(line, wside)) + { + P_DamageLinedef(line, mo, mo->GetMissileDamage((mo->flags4 & MF4_STRIFEDAMAGE) ? 3 : 7, 1), mo->DamageType, wside, mo->Pos()); + } + } + if (mo->flags3 & MF3_EXPLOCOUNT) { if (++mo->threshold < mo->DefThreshold) @@ -2689,30 +2714,50 @@ double P_XYMovement (AActor *mo, DVector2 scroll) explode: // explode a missile bool onsky = false; - if (tm.ceilingline && - tm.ceilingline->backsector && - tm.ceilingline->backsector->GetTexture(sector_t::ceiling) == skyflatnum && - mo->Z() >= tm.ceilingline->backsector->ceilingplane.ZatPoint(mo->PosRelative(tm.ceilingline))) + if (tm.ceilingline && + tm.ceilingline->backsector && + tm.ceilingline->backsector->GetTexture(sector_t::ceiling) == skyflatnum && + mo->Z() >= tm.ceilingline->backsector->ceilingplane.ZatPoint(mo->PosRelative(tm.ceilingline))) + { + if (!(mo->flags3 & MF3_SKYEXPLODE)) { - if (!(mo->flags3 & MF3_SKYEXPLODE)) - { - // Hack to prevent missiles exploding against the sky. - // Does not handle sky floors. - mo->Destroy(); - return Oldfloorz; - } - else onsky = true; + // Hack to prevent missiles exploding against the sky. + // Does not handle sky floors. + mo->Destroy(); + return Oldfloorz; } - // [RH] Don't explode on horizon lines. - if (mo->BlockingLine != NULL && mo->BlockingLine->special == Line_Horizon) + else onsky = true; + } + // [RH] Don't explode on horizon lines. + if (mo->BlockingLine != NULL && mo->BlockingLine->special == Line_Horizon) + { + if (!(mo->flags3 & MF3_SKYEXPLODE)) { - if (!(mo->flags3 & MF3_SKYEXPLODE)) - { - mo->Destroy(); - return Oldfloorz; - } - else onsky = true; + mo->Destroy(); + return Oldfloorz; } + else onsky = true; + } + if (!mo->BlockingLine && !BlockingMobj) // hit floor or ceiling while XY movement + { + int hitpart = -1; + sector_t* hitsector = nullptr; + // check against floor + if (tm.floorsector && mo->Z() < tm.floorsector->floorplane.ZatPoint(tm.pos.XY())) + { + hitpart = SECPART_Floor; + hitsector = tm.floorsector; + if (hitsector->healthfloor > 0 && P_CheckSectorVulnerable(hitsector, hitpart)) + P_DamageSector(hitsector, mo, mo->GetMissileDamage((mo->flags4 & MF4_STRIFEDAMAGE) ? 3 : 7, 1), mo->DamageType, hitpart, mo->Pos()); + } + if (tm.ceilingsector && mo->Z() + mo->Height > tm.ceilingsector->ceilingplane.ZatPoint(tm.pos.XY())) + { + hitpart = SECPART_Ceiling; + hitsector = tm.ceilingsector; + if (hitsector->healthceiling > 0 && P_CheckSectorVulnerable(hitsector, hitpart)) + P_DamageSector(hitsector, mo, mo->GetMissileDamage((mo->flags4 & MF4_STRIFEDAMAGE) ? 3 : 7, 1), mo->DamageType, hitpart, mo->Pos()); + } + } P_ExplodeMissile (mo, mo->BlockingLine, BlockingMobj, onsky); return Oldfloorz; } @@ -3105,6 +3150,9 @@ void P_ZMovement (AActor *mo, double oldfloorz) else onsky = true; } P_HitFloor (mo); + // hit floor: direct damage callback + if (mo->Sector->healthfloor > 0 && P_CheckSectorVulnerable(mo->Sector, SECPART_Floor)) + P_DamageSector(mo->Sector, mo, mo->GetMissileDamage((mo->flags4 & MF4_STRIFEDAMAGE) ? 3 : 7, 1), mo->DamageType, SECPART_Floor, mo->Pos()); P_ExplodeMissile (mo, NULL, NULL, onsky); return; } @@ -3208,6 +3256,9 @@ void P_ZMovement (AActor *mo, double oldfloorz) } else onsky = true; } + // hit ceiling: direct damage callback + if (mo->Sector->healthceiling > 0 && P_CheckSectorVulnerable(mo->Sector, SECPART_Ceiling)) + P_DamageSector(mo->Sector, mo, mo->GetMissileDamage((mo->flags4 & MF4_STRIFEDAMAGE) ? 3 : 7, 1), mo->DamageType, SECPART_Ceiling, mo->Pos()); P_ExplodeMissile (mo, NULL, NULL, onsky); return; } diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 32d3dd5ff..4a48afbb6 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -101,6 +101,7 @@ #include "edata.h" #endif #include "events.h" +#include "p_destructible.h" #include "types.h" #include "i_time.h" #include "scripting/vm/vm.h" @@ -4124,6 +4125,7 @@ void P_SetupLevel (const char *lumpname, int position, bool newGame) screen->mVertexData->CreateVBO(); SWRenderer->SetColormap(); //The SW renderer needs to do some special setup for the level's default colormap. InitPortalGroups(); + P_InitHealthGroups(); times[16].Clock(); if (reloop) P_LoopSidedefs (false); diff --git a/src/p_udmf.cpp b/src/p_udmf.cpp index c7268318c..f8b6d2c4d 100644 --- a/src/p_udmf.cpp +++ b/src/p_udmf.cpp @@ -1120,6 +1120,21 @@ public: tagstring = CheckString(key); break; + case NAME_Health: + ld->health = CheckInt(key); + break; + + case NAME_DamageSpecial: + Flag(ld->activation, SPAC_Damage, key); + break; + + case NAME_DeathSpecial: + Flag(ld->activation, SPAC_Death, key); + break; + + case NAME_HealthGroup: + ld->healthgroup = CheckInt(key); + break; default: break; @@ -1362,7 +1377,6 @@ public: double scroll_floor_y = 0; FName scroll_floor_type = NAME_None; - memset(sec, 0, sizeof(*sec)); sec->lightlevel = 160; sec->SetXScale(sector_t::floor, 1.); // [RH] floor and ceiling scaling @@ -1769,6 +1783,22 @@ public: // These two are used by Eternity for something I do not understand. //case NAME_portal_ceil_useglobaltex: //case NAME_portal_floor_useglobaltex: + + case NAME_HealthFloor: + sec->healthfloor = CheckInt(key); + break; + + case NAME_HealthCeiling: + sec->healthceiling = CheckInt(key); + break; + + case NAME_HealthFloorGroup: + sec->healthfloorgroup = CheckInt(key); + break; + + case NAME_HealthCeilingGroup: + sec->healthceilinggroup = CheckInt(key); + break; default: break; diff --git a/src/r_defs.h b/src/r_defs.h index ffd504ea4..f3da27262 100644 --- a/src/r_defs.h +++ b/src/r_defs.h @@ -266,17 +266,21 @@ struct FRemapTable; enum { - SECSPAC_Enter = 1, // Trigger when player enters - SECSPAC_Exit = 2, // Trigger when player exits - SECSPAC_HitFloor = 4, // Trigger when player hits floor - SECSPAC_HitCeiling = 8, // Trigger when player hits ceiling - SECSPAC_Use = 16, // Trigger when player uses - SECSPAC_UseWall = 32, // Trigger when player uses a wall - SECSPAC_EyesDive = 64, // Trigger when player eyes go below fake floor - SECSPAC_EyesSurface = 128, // Trigger when player eyes go above fake floor - SECSPAC_EyesBelowC = 256, // Trigger when player eyes go below fake ceiling - SECSPAC_EyesAboveC = 512, // Trigger when player eyes go above fake ceiling - SECSPAC_HitFakeFloor= 1024, // Trigger when player hits fake floor + SECSPAC_Enter = 1<< 0, // Trigger when player enters + SECSPAC_Exit = 1<< 1, // Trigger when player exits + SECSPAC_HitFloor = 1<< 2, // Trigger when player hits floor + SECSPAC_HitCeiling = 1<< 3, // Trigger when player hits ceiling + SECSPAC_Use = 1<< 4, // Trigger when player uses + SECSPAC_UseWall = 1<< 5, // Trigger when player uses a wall + SECSPAC_EyesDive = 1<< 6, // Trigger when player eyes go below fake floor + SECSPAC_EyesSurface = 1<< 7, // Trigger when player eyes go above fake floor + SECSPAC_EyesBelowC = 1<< 8, // Trigger when player eyes go below fake ceiling + SECSPAC_EyesAboveC = 1<< 9, // Trigger when player eyes go above fake ceiling + SECSPAC_HitFakeFloor= 1<<10, // Trigger when player hits fake floor + SECSPAC_DamageFloor = 1<<11, // Trigger when floor is damaged + SECSPAC_DamageCeiling=1<<12, // Trigger when ceiling is damaged + SECSPAC_DeathFloor = 1<<13, // Trigger when floor has 0 hp + SECSPAC_DeathCeiling= 1<<14, // Trigger when ceiling has 0 hp }; struct secplane_t @@ -1090,6 +1094,13 @@ public: INVALIDATE_OTHER = 2 }; + // [ZZ] these are for destructible sectors. + // default is 0, which means no special behavior + int healthfloor; + int healthceiling; + int healthfloorgroup; + int healthceilinggroup; + }; struct ReverbContainer; @@ -1307,6 +1318,8 @@ struct line_t unsigned portalindex; unsigned portaltransferred; AutomapLineStyle automapstyle; + int health; // [ZZ] for destructible geometry (0 = no special behavior) + int healthgroup; // [ZZ] this is the "destructible object" id DVector2 Delta() const { diff --git a/wadsrc/static/mapinfo/common.txt b/wadsrc/static/mapinfo/common.txt index d3764371e..e83fc78f4 100644 --- a/wadsrc/static/mapinfo/common.txt +++ b/wadsrc/static/mapinfo/common.txt @@ -88,6 +88,10 @@ DoomEdNums 9503 = "$SetCeilingSlope" 9510 = "$CopyFloorPlane" 9511 = "$CopyCeilingPlane" + 9600 = SecActDamageFloor + 9601 = SecActDamageCeiling + 9602 = SecActDeathFloor + 9603 = SecActDeathCeiling 9800 = PointLight 9801 = PointLightPulse 9802 = PointLightFlicker diff --git a/wadsrc/static/zscript/shared/sectoraction.txt b/wadsrc/static/zscript/shared/sectoraction.txt index fb7edfb19..6b93d9805 100644 --- a/wadsrc/static/zscript/shared/sectoraction.txt +++ b/wadsrc/static/zscript/shared/sectoraction.txt @@ -4,17 +4,21 @@ class SectorAction : Actor // self class uses health to define the activation type. enum EActivation { - SECSPAC_Enter = 1, - SECSPAC_Exit = 2, - SECSPAC_HitFloor = 4, - SECSPAC_HitCeiling = 8, - SECSPAC_Use = 16, - SECSPAC_UseWall = 32, - SECSPAC_EyesDive = 64, - SECSPAC_EyesSurface = 128, - SECSPAC_EyesBelowC = 256, - SECSPAC_EyesAboveC = 512, - SECSPAC_HitFakeFloor= 1024, + SECSPAC_Enter = 1<< 0, + SECSPAC_Exit = 1<< 1, + SECSPAC_HitFloor = 1<< 2, + SECSPAC_HitCeiling = 1<< 3, + SECSPAC_Use = 1<< 4, + SECSPAC_UseWall = 1<< 5, + SECSPAC_EyesDive = 1<< 6, + SECSPAC_EyesSurface = 1<< 7, + SECSPAC_EyesBelowC = 1<< 8, + SECSPAC_EyesAboveC = 1<< 9, + SECSPAC_HitFakeFloor = 1<<10, + SECSPAC_DamageFloor = 1<<11, + SECSPAC_DamageCeiling = 1<<12, + SECSPAC_DeathFloor = 1<<13, + SECSPAC_DeathCeiling = 1<<14 }; default @@ -200,6 +204,54 @@ class SecActHitFakeFloor : SectorAction } } +// Triggered when sector's floor is damaged ---------------------------------- +class SecActDamageFloor : SectorAction +{ + Default + { + Health SECSPAC_DamageFloor; + } + + // [ZZ] damage is unconditional, so this as well + override bool CanTrigger (Actor triggerer) { return !!special; } +} + +// Triggered when sector's ceiling is damaged ---------------------------------- +class SecActDamageCeiling : SectorAction +{ + Default + { + Health SECSPAC_DamageCeiling; + } + + // [ZZ] damage is unconditional, so this as well + override bool CanTrigger (Actor triggerer) { return !!special; } +} + +// Triggered when sector's floor is reduced to 0 hp ---------------------------------- +class SecActDeathFloor : SectorAction +{ + Default + { + Health SECSPAC_DeathFloor; + } + + // [ZZ] damage is unconditional, so this as well + override bool CanTrigger (Actor triggerer) { return !!special; } +} + +// Triggered when sector's ceiling is reduced to 0 hp ---------------------------------- +class SecActDeathCeiling : SectorAction +{ + Default + { + Health SECSPAC_DeathCeiling; + } + + // [ZZ] damage is unconditional, so this as well + override bool CanTrigger (Actor triggerer) { return !!special; } +} + //========================================================================== // // Music changer. Uses the sector action class to do its job