Don't use tags, and don't use heartbeats. The first is too limiting, and the latter unnecessary. Tags were the old way of identifying area characteristics, waaaaaaay back in the day, for one simple reason: people were unable to set ints in the toolset, it had to be done ingame. Now, however, you don't have to operate under that limitation. Variables are FAR more flexible.
We do what you're wanting to do on HG, using a combination of variables. There's one if an area is underwater, another if immortals are subject to respawn there, and still another if the area applies some kind of penalty. All of those are handy for when you want to do something to a group of areas. For things that only apply in a single area, we have our generic area script for that event fire off a custom script specified by variable on the area. Here, by way of example, is our area enter script, which is on most areas in the mod, save for a few very old ones which don't need it. It handles everything that needs handling in an area entry script, like NPC spawnins, despawn tracking (antiexploit), and much much more:
#include "hg_inc"
#include "hg_area_inc"
#include "hg_antiex_inc"
#include "fky_environ_inc"
#include "ac_qstatus_inc"
#include "tsk_inc"
void DoAreaEffects (object oArea, object oPC, int nDamAmount, int nDamType, int nDamVis, string sMessage) {
if (GetArea(oPC) != oArea)
return;
if (!GetIsDead(oPC) && !GetPlotFlag(oPC)) {
if (nDamType < 0) {
if (nDamType == -1) {
int nBreath = GetCanBreatheWater(oPC, TRUE);
if (nBreath == 2)
return;
if (!nBreath) {
if (GetLocalInt(oPC, "Area_WaterBreath_Warning") < 3) {
FloatingTextStringOnCreature("You cannot hold your breath much longer!", oPC, FALSE);
AddLocalInt(oPC, "Area_WaterBreath_Warning", 1);
} else {
FloatingTextStringOnCreature("You can no longer hold your breath!", oPC, FALSE);
DeleteLocalInt(oPC, "Area_WaterBreath_Warning");
ApplyEffectToObject(DURATION_TYPE_INSTANT, SupernaturalEffect(EffectDeath()), oPC);
}
} else
DeleteLocalInt(oPC, "Area_WaterBreath_Warning");
} else if (nDamType == -2) {
int nLev = GetCanLevitate(oPC);
if (nLev == 2)
return;
if (!nLev) {
if (!GetIsResting(oPC))
FloatingTextStringOnCreature("You are unable to control your motion in the air!", oPC, FALSE);
RemoveEffectsOfType(EFFECT_TYPE_CUTSCENEIMMOBILIZE, oPC, oArea);
effect eEff = SupernaturalEffect(EffectCutsceneImmobilize());
DelayCommand(0.01, ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eEff, oPC, 9.0));
}
} else if (nDamType == -3) {
/* passwall; do nothing */
return;
} else if (nDamType == -4) {
int nFirewalk = GetCanFirewalk(oPC);
if (nFirewalk == 2)
return;
if (!nFirewalk) {
FloatingTextStringOnCreature("You are unable to breathe elemental fire!", oPC, FALSE);
ApplyDirectDamage(oPC, (GetMaxHitPoints(oPC) * 2) / 5, DAMAGE_TYPE_INTERNAL,
DAMAGE_POWER_ENERGY, "Elemental Fire", "You burn to a crisp!");
ApplyVisualToObject(VFX_IMP_FLAME_M, oPC);
}
}
} else {
effect eDam = EffectDamage(d10(nDamAmount), nDamType);
effect eVis = EffectVisualEffect(nDamVis);
ApplyEffectToObject(DURATION_TYPE_INSTANT, eDam, oPC);
ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oPC);
if (sMessage != "")
FloatingTextStringOnCreature(sMessage, oPC, FALSE);
}
}
DelayCommand(6.0, DoAreaEffects(oArea, oPC, nDamAmount, nDamType, nDamVis, sMessage));
}
void DoDust (object oPC, object oItem) {
SetPlotFlag(oItem, FALSE);
DestroyObject(oItem);
CreateItemOnObject("drowdust", oPC);
}
object GetVFXLoopTarget (object oSource, string sTarget) {
int nCount = 1;
struct SubString ss = GetFirstSubString(sTarget, "#");
if (ss.rest != "") {
}
return GetNearestObjectByTag(ss.first, oSource, nCount);
}
float GetVFXLoopVaryingFloat (float fRand) {
int nVary = FloatToInt(fRand * 100.0);
nVary = Random((nVary * 2) + 1) - nVary;
return (nVary * 0.01);
}
void VoidBroadcastProjectileToObject (object oSource, object oTarget, int nSpellId, int nDelay) {
BroadcastProjectileToObject(oSource, oTarget, nSpellId, nDelay);
}
void DoVFXLoop (object oVFX, int nVis, float fCyc, float fDur, string sTarget, int bBeam,
float fCycRand, float fDurRand, int nMulti, float fMultiInt, float fMultiRand) {
if (GetLocalInt(OBJECT_SELF, "Area_Clear")) {
AddLocalInt(OBJECT_SELF, "Area_VFX_Active", -1);
return;
}
if (nMulti > 1) {
/* ugly to have a second copy, but removes unnecessary delaycommands */
int i;
float fDelay = 0.0;
for (i = 0; i < nMulti; i++) {
float fDurVary = GetVFXLoopVaryingFloat(fDurRand);
if (sTarget != "") {
object oTarget = GetVFXLoopTarget(oVFX, sTarget);
if (bBeam) {
effect eBeam = EffectBeam(nVis, oVFX, BODY_NODE_CHEST, (bBeam == 2));
DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eBeam, oTarget, fDur + fDurVary));
} else if (nVis < 0) {
DelayCommand(fDelay, VoidBroadcastProjectileToObject(oVFX, oTarget, -nVis, FloatToInt((fDur + fDurVary) * 1000.0)));
} else if (fDur > 0.0) {
effect eVis = EffectVisualEffect(nVis);
DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eVis, oTarget, fDur + fDurVary));
} else {
DelayCommand(fDelay, ApplyVisualToObject(nVis, oTarget));
}
} else {
if (fDur > 0.0) {
effect eVis = EffectVisualEffect(nVis);
DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eVis, oVFX, fDur + fDurVary));
} else {
DelayCommand(fDelay, ApplyVisualToObject(nVis, oVFX));
}
}
fDelay += fMultiInt + GetVFXLoopVaryingFloat(fMultiRand);
}
} else {
float fDurVary = GetVFXLoopVaryingFloat(fDurRand);
if (sTarget != "") {
object oTarget = GetVFXLoopTarget(oVFX, sTarget);
if (bBeam) {
effect eBeam = EffectBeam(nVis, oVFX, BODY_NODE_CHEST, (bBeam == 2));
ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eBeam, oTarget, fDur + fDurVary);
} else if (nVis < 0) {
BroadcastProjectileToObject(oVFX, oTarget, -nVis, FloatToInt((fDur + fDurVary) * 1000.0));
} else if (fDur > 0.0) {
effect eVis = EffectVisualEffect(nVis);
ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eVis, oTarget, fDur + fDurVary);
} else {
ApplyVisualToObject(nVis, oTarget);
}
} else {
if (fDur > 0.0) {
effect eVis = EffectVisualEffect(nVis);
ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eVis, oVFX, fDur + fDurVary);
} else {
ApplyVisualToObject(nVis, oVFX);
}
}
}
float fCycVary = GetVFXLoopVaryingFloat(fCycRand);
DelayCommand(fCyc + fCycVary, DoVFXLoop(oVFX, nVis, fCyc, fDur, sTarget, bBeam, fCycRand, fDurRand, nMulti, fMultiInt, fMultiRand));
}
void main () {
object oPC = GetEnteringObject();
object oArea = GetArea(oPC);
if (GetIsPC(oPC)) {
DeleteLocalInt(oArea, "Area_Clear");
int bReveal = GetLocalInt(oArea, "Area_Reveal");
if (bReveal > 0)
ExploreAreaForPlayer(oArea, oPC, TRUE);
}
if (GetLocalString(oPC, "FKY_CHAT_PASSWORD_IP") == "CHECK")
AssignCommand(GenCreator(), ApplyPasswordHold(oPC));
/* execute specific onenter script if specified */
string sScript = GetLocalString(oArea, "Area_OnEnter");
if (sScript != "")
ExecuteScript(sScript, oArea);
/* PC-only stuff below here */
if (!GetIsPC(oPC))
return;
if (GetLocalInt(oArea, "Area_VFX_Objects") > 0 &&
GetLocalInt(oArea, "Area_VFX_Active") == 0) {
int i, nObjects = GetLocalInt(oArea, "Area_VFX_Objects");
object oVFX;
for (i = 0; i < nObjects; i++) {
oVFX = GetLocalObject(oArea, "Area_VFX_Object_" + IntToString(i));
if (GetIsObjectValid(oVFX)) {
int nVis = GetLocalInt(oVFX, "VFX_Loop");
int nMulti = GetLocalInt(oVFX, "VFX_Loop_Multi");
int bBeam = GetLocalInt(oVFX, "VFX_Loop_Beam");
float fCyc = GetLocalFloat(oVFX, "VFX_Loop_Cycle");
float fCycRand = GetLocalFloat(oVFX, "VFX_Loop_Cycle_Vary");
float fDur = GetLocalFloat(oVFX, "VFX_Loop_Dur");
float fDurRand = GetLocalFloat(oVFX, "VFX_Loop_Dur_Vary");
float fMultiInt = GetLocalFloat(oVFX, "VFX_Loop_Multi_Int");
float fMultiRand = GetLocalFloat(oVFX, "VFX_Loop_Multi_Int_Vary");
string sTarget = GetLocalString(oVFX, "VFX_Loop_Target");
AddLocalInt(oArea, "Area_VFX_Active", 1);
AssignCommand(oArea, DelayCommand(fCyc, DoVFXLoop(oVFX, nVis, fCyc, fDur, sTarget, bBeam, fCycRand, fDurRand, nMulti, fMultiInt, fMultiRand)));
}
}
}
/* if the PC is not allowed in the area, boot them and return */
if (!CheckAreaTagRequirement(oArea, oPC))
return;
string sQuest = GetLocalString(oArea, "Area_Quest");
if (sQuest != "")
QSAddQuestStatus(sQuest, 1);
/* for Hell and Limbo areas, set the Succor_ local */
if (GetLocalInt(oArea, "notele") > 0)
SetLocalInt(oPC, "Succor_" + GetResRef(oArea), 1);
/* area damage/underwater areas */
/* TODO: enhance this to be more generic, e.g. Hells penalties */
if (!GetIsDM(oPC)) {
int nDamType = GetLocalInt(oArea, "Area_Damage_Type");
if (GetLocalInt(oArea, "Area_Underwater"))
nDamType = -1;
else if (GetLocalInt(oArea, "Area_Aerial"))
nDamType = -2;
else if (GetLocalInt(oArea, "Area_Passwall"))
nDamType = -3;
else if (GetLocalInt(oArea, "Area_Firewalk"))
nDamType = -4;
if (nDamType) {
int nDamAmount = GetLocalInt(oArea, "Area_Damage_Amount");
int nDamVis = GetLocalInt(oArea, "Area_Damage_VFX");
string sMessage = GetLocalString(oArea, "Area_Damage_Message");
AssignCommand(oArea, DoAreaEffects(oArea, oPC, nDamAmount, nDamType, nDamVis, sMessage));
}
/* destroy items that cannot survive in daylight */
if (GetIsDay() &&
!GetIsAreaInterior(oArea) &&
GetIsAreaAboveGround(oArea)) {
int i;
object oItem;
for (i = 0; i < NUM_INVENTORY_SLOTS; i++) {
oItem = GetItemInSlot(i, oPC);
if (GetStringLeft(GetTag(oItem), 4) == "asdf")
DoDust(oPC, oItem);
}
if (GetIsObjectValid(oItem = GetItemPossessedBy(oPC, "sealofshadows")))
DestroyObject(oItem);
}
}
/* spawn anything specified in area locals */
int nSpawns = GetLocalInt(oArea, "Area_Spawns");
string sTask;
struct IntList li;
if (nSpawns > 0) {
int i, nType;
string sKey, sTag, sRes, sLoc, sLocType;
object oSpawn;
location lSpawn;
for (i = 1; i <= nSpawns; i++) {
sKey = "Area_Spawn_" + IntToString(i);
sTag = GetLocalString(oArea, sKey + "_Tag");
/* if the object has already been spawned, skip it */
if (sTag != "") {
if (GetIsObjectValid(GetNearestObjectByTag(sTag, oPC)))
continue;
} else {
if (GetIsObjectValid(GetLocalObject(oArea, sKey + "_Obj")))
continue;
}
sRes = GetLocalString(oArea, sKey + "_Res");
sLoc = GetLocalString(oArea, sKey + "_Loc");
nType = GetLocalInt(oArea, sKey + "_Type");
if (nType == 0)
nType = OBJECT_TYPE_CREATURE;
sLocType = GetStringLeft(sLoc, 1);
if (sLocType == "!")
lSpawn = GetLocation(GetNearestObjectByTag(GetSubString(sLoc, 1, 64), oPC));
else if (sLocType == "@")
lSpawn = GetLocation(GetWaypointByTag(GetSubString(sLoc, 1, 64)));
else if (sLocType == "&") {//designates task creature, only spawn if pc requires (uses waypoint) ex: tsk_cr_1_2
sTask = GetStringRight(sRes, (GetStringLength(sRes) - 7));
li = GetIntList(sTask, "_");
if (!GetPCHasTaskActive(oPC, li.i0) || (GetTaskCompleted(oPC, li.i0) != (li.i1 - 1)))//if they don't have the task active, don't spawn
continue;
else
lSpawn = GetLocation(GetWaypointByTag(GetSubString(sLoc, 1, 64)));
} else
continue;
if (GetAreaFromLocation(lSpawn) == oArea) {
oSpawn = CreateObject(nType, sRes, lSpawn, FALSE, sTag);
SetLocalObject(oArea, sKey + "_Obj", oSpawn);
}
}
}
/* respawn loot or other other respawnables */
nSpawns = GetLocalInt(oArea, "Area_Respawns");
if (nSpawns > 0) {
int i, nType, nRespawnTime, nUptime = GetLocalInt(GetModule(), "uptime");
string sKey, sVar;
object oSpawn;
for (i = 1; i <= nSpawns; i++) {
sKey = IntToString(i);
nRespawnTime = GetLocalInt(oArea, "Area_Respawn_" + sKey);
if (nRespawnTime <= 0 ||
nRespawnTime > nUptime ||
GetIsObjectValid(GetLocalObject(oArea, "Area_Respawn_Obj_" + sKey)))
continue;
oSpawn = CreateObject(GetLocalInt(oArea, "Area_Respawn_Type_" + sKey),
GetLocalString(oArea, "Area_Respawn_Res_" + sKey),
GetLocalLocation(oArea, "Area_Respawn_Loc_" + sKey));
SetLocalObject(oArea, "Area_Respawn_Obj_" + sKey, oSpawn);
if ((sVar = GetLocalString(oArea, "Area_Respawn_Var_" + sKey)) != "")
AssignCommand(oArea, DelayCommand(0.0, RestoreLocals(oSpawn, sVar)));
}
}
/* clear despawn setting on area */
if (GetLocalInt(oArea, "DespawnTrack")) {
int nDespawnTime = GetLocalInt(oArea, "DespawnTime");
if (nDespawnTime) {
int nUptime = GetLocalInt(GetModule(), "uptime");
if ((nUptime - 3600) > nDespawnTime) {
DeleteLocalInt(oArea, "DespawnTime");
DeleteLocalInt(oArea, "DespawnCount");
}
}
}
}
This setup allows you to specific things with much much more detail than a tag-based setup (trust me, I tried using on in the first group of areas I made, back in the day, and we wound up converting to variable-based methods just a short time later). Here, for example, are all the variables set on one of our Abyss areas, the reef at the base of Demogorgon's fortress Abysm:
Abyss Level int 14
Area_OnEnter string aby_enter
Area_Penalties int 4
Area_Rest_Monster string aby_ixitpriest aby_ixitcultist aby_vampiricixit
Area_SpellHook string zwund_spellhook
Area_Underwater int 1
DespawnTrack int 1
HellLootSpawn int 4
Loot int 10
LootF int 66
NoImmo int 5
notele int 2
Imagine trying to store all that in a tag, one able to hold various other settings. It would be absurdly long, and much harder to make sense of, let alone change on the fly. There is simply no way in which tags are superior to variables for this purpose.
Funky