There are ways to do what you want, but they're fairly involved. Here's our sample code for mud that slows players while they're in it:
/* Shedaklah - Gasping Crater
* Slowing Mud (height 11.5)
*/
case 31: {
vector vPos = GetPosition(oTarget);
if (vPos.z < 11.5 && !GetCanPasswall(oTarget)) {
effect eEff = EffectSpellImmunity(HGSPELL_UNUSED);
eEff = SupernaturalEffect(eEff);
SetEffectSpellId(eEff, HGEFFECT_LETHARGY);
if (!GetHasSpellEffect(HGEFFECT_LETHARGY, oTarget))
FloatingTextStringOnCreature("The mud sticks to you and slows you down!", oTarget, FALSE);
ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eEff, oTarget, 12.0);
}
}
Basically, we set a spell id on an otherwise unused/non-existant spell immunity effect, creating a new spell id effect. That, in itself, requires nwnx_structs, though you can get around that by using a custom effect creator object and checking something unique, like tag or resref, of GetEffectCreator.
That's just the start, because, by itself, that effect does nothing, other than serve as a marker.
We have the following scripts in our central include:
int GetAdjustedMovementRate (int nRate, int nAdjust) {
if (nRate == MOVEMENT_RATE_PC)
nRate = MOVEMENT_RATE_NORMAL;
if (nRate == MOVEMENT_RATE_DM_FAST ||
nRate == MOVEMENT_RATE_IMMOBILE ||
nRate == MOVEMENT_RATE_EXTRA_SLOW ||
nRate == MOVEMENT_RATE_SUPER_SLOW ||
nRate == MOVEMENT_RATE_GLACIAL)
return nRate;
switch (nRate) {
case MOVEMENT_RATE_VERY_SLOW:
nRate = 0;
break;
case MOVEMENT_RATE_SLOW:
nRate = 1;
break;
case MOVEMENT_RATE_RELAXED:
nRate = 2;
break;
case MOVEMENT_RATE_FAST:
nRate = 4;
break;
case MOVEMENT_RATE_FASTER:
nRate = 5;
break;
case MOVEMENT_RATE_VERY_FAST:
nRate = 6;
break;
case MOVEMENT_RATE_EXTRA_FAST:
case MOVEMENT_RATE_SUPER_FAST:
case MOVEMENT_RATE_HYPER_FAST:
case MOVEMENT_RATE_LIGHTSPEED:
case MOVEMENT_RATE_RIDICULOUS:
case MOVEMENT_RATE_LUDICROUS:
nRate = 7 + (nRate - MOVEMENT_RATE_EXTRA_FAST);
break;
default:
nRate = 3;
break;
}
nRate += nAdjust;
if (nRate > 12)
nRate = 12;
else if (nRate < 0)
nRate = 0;
switch (nRate) {
case 0:
nRate = MOVEMENT_RATE_VERY_SLOW;
break;
case 1:
nRate = MOVEMENT_RATE_SLOW;
break;
case 2:
nRate = MOVEMENT_RATE_RELAXED;
break;
case 4:
nRate = MOVEMENT_RATE_FAST;
break;
case 5:
nRate = MOVEMENT_RATE_FASTER;
break;
case 6:
nRate = MOVEMENT_RATE_VERY_FAST;
break;
case 7: /* MOVEMENT_RATE_EXTRA_FAST */
case 8: /* MOVEMENT_RATE_SUPER_FAST */
case 9: /* MOVEMENT_RATE_HYPER_FAST */
case 10: /* MOVEMENT_RATE_LIGHTSPEED */
case 11: /* MOVEMENT_RATE_RIDICULOUS */
case 12: /* MOVEMENT_RATE_LUDICROUS */
nRate = MOVEMENT_RATE_EXTRA_FAST + (nRate - 7);
break;
default:
nRate = MOVEMENT_RATE_NORMAL;
break;
}
return nRate;
}
int GetBaseMovementRate (object oPC) {
int nRate = GetLocalInt(oPC, "SubraceMovementRate");
if (GetQuasiclass(oPC) == QUASICLASS_ACCURSED_PARIAH)
nRate = MOVEMENT_RATE_NORMAL;
else if (GetLocalInt(oPC, "RDDLevel") >= 30)
nRate = MOVEMENT_RATE_FAST;
if (nRate <= 0)
nRate = MOVEMENT_RATE_NORMAL;
if (GetLocalInt(oPC, "StatArtifactUsed") == HGARTIFACT_TEAR_OF_SELUNE)
nRate = GetAdjustedMovementRate(nRate, 2);
if (GetHasFeat(FEAT_EPIC_BLINDING_SPEED, oPC))
nRate = GetAdjustedMovementRate(nRate, 1);
if (GetHasFeat(HGFEAT_Y_QUICKNESS, oPC))
nRate = GetAdjustedMovementRate(nRate, 1);
return nRate;
}
void RecalculateMovementRate (object oPC) {
if (!GetIsPC(oPC) || GetIsDM(oPC))
return;
int nRate;
if (GetHasSpellImmunity(SPELLABILITY_DW_DEFENSIVE_STANCE, oPC) ||
GetHasSpellEffect(HGSPELL_VISCID_GLOB, oPC)) {
nRate = MOVEMENT_RATE_GLACIAL;
} else if ((GetHasSpellEffect(HGEFFECT_LETHARGY, oPC) ||
GetHasSpellEffect(HGEFFECT_GNOMISH_INVENTOR_KD_IMMUNITY, oPC)) &&
GetLocalInt(oPC, "StatArtifactUsed") != HGARTIFACT_TEAR_OF_SELUNE) {
nRate = MOVEMENT_RATE_SLOW;
} else {
nRate = GetBaseMovementRate(oPC);
if (GetHasSpellEffect(SPELL_EXPEDITIOUS_RETREAT, oPC) || GetHasSpellEffect(SPELL_FREEDOM_OF_MOVEMENT, oPC))
nRate = GetAdjustedMovementRate(nRate, GetLocalInt(oPC, "MovementRateBonus"));
}
if (nRate == MOVEMENT_RATE_NORMAL)
nRate = MOVEMENT_RATE_PC;
SetMovementRate(oPC, nRate);
}
These moverate consts are in nwnx_functions:
const int MOVEMENT_RATE_PC = 0;
const int MOVEMENT_RATE_IMMOBILE = 1;
const int MOVEMENT_RATE_VERY_SLOW = 2;
const int MOVEMENT_RATE_SLOW = 3;
const int MOVEMENT_RATE_NORMAL = 4;
const int MOVEMENT_RATE_FAST = 5;
const int MOVEMENT_RATE_VERY_FAST = 6;
const int MOVEMENT_RATE_DEFAULT = 7;
const int MOVEMENT_RATE_DM_FAST = 8;
const int MOVEMENT_RATE_FASTER = 9;
const int MOVEMENT_RATE_EXTRA_FAST = 10;
const int MOVEMENT_RATE_SUPER_FAST = 11;
const int MOVEMENT_RATE_HYPER_FAST = 12;
const int MOVEMENT_RATE_LIGHTSPEED = 13;
const int MOVEMENT_RATE_RIDICULOUS = 14;
const int MOVEMENT_RATE_LUDICROUS = 15;
const int MOVEMENT_RATE_RELAXED = 16;
const int MOVEMENT_RATE_EXTRA_SLOW = 17;
const int MOVEMENT_RATE_SUPER_SLOW = 18;
const int MOVEMENT_RATE_GLACIAL = 19;
We simply recalc the movement rate for the pc at certain places in the mod:
fky_deathprocess (56): RecalculateMovementRate(oPC);
fky_restprocess (264): RecalculateMovementRate(oPC);
hgs_viscidglob (64): RecalculateMovementRate(si.target);
hg_inc (3661): void RecalculateMovementRate (object oPC) {
hg_mod_heartbeat (36): RecalculateMovementRate(oPC);
nw_s0_freemove (107): DelayCommand(0.5, RecalculateMovementRate(si.target));
qc_gi_inc (642): DelayCommand(0.1, RecalculateMovementRate(oTarget));
x0_s0_shield (47): DelayCommand(0.1, RecalculateMovementRate(si.caster));
fky_deathprocess is called whenever a pc is brought back to life. It's essentially a 'rez event', and you'll need one to do this. fky_restprocess is the same, though that's a convenience script, because we sometimes call it other than when a pc rests (ForceRests, among other things):
ag_wandering_mon (92): ExecuteScript("fky_restprocess", oPC);
asmoset001 (26): ExecuteScript("fky_restprocess", oPC);
biorejuvenator (20): ExecuteScript("fky_restprocess", oPC);
fky_chat_dm_comm (1140): if ((!VerifyDMKey(oDMTarget)) && (!VerifyAdminKey(oDMTarget)) || (oDMTarget == oDMPC)) {ForceRest(oDMTarget); ExecuteScript("fky_restprocess", oDMTarget);}
hellhiduse (105): ExecuteScript("fky_restprocess", oPC);
hgll_start_dlg (9): - old altar had forced rest and fky_restprocess onenter, omitted from this setup,
hg_inc (4302): ExecuteScript("fky_restprocess", oPC);
legendaltarlimit (14): ExecuteScript("fky_restprocess", oPC);
paragon_spell (1477): /* counter spell, recalc done each rest in fky_restprocess */
Firing RecalculateMovementRate in the module heartbeat, by the way, also prevents a known speed exp%lo@it. The loop is straightfoward enough:
void main() {
object oMod = GetModule();
object oMes = GetMessenger();
int nUptime = GetLocalInt(oMod, "uptime");
int nMemory = GetProcessMemoryUsage();
int nMessages = 0, nPlayers = 0;
string sServer = GetLocalString(oMod, "ServerNumber");
string sBootTime = IntToString(GetLocalInt(oMod, "boottime"));
{
object oPC;
for (oPC = GetFirstPC(); GetIsObjectValid(oPC); oPC = GetNextPC()) {
nPlayers++;
RecalculateMovementRate(oPC);
RecalculateDexModifier(oPC);
int nAlarm = GetLocalInt(oPC, "AlarmUptime");
if (nAlarm > 0 && nAlarm <= nUptime) {
DeleteLocalInt(oPC, "AlarmUptime");
SendChatLogMessage(oPC, C_PINK + "[Alarm] " + GetLocalString(oPC, "AlarmMessage") + C_END, oMes, 4);
}
}
SetLocalInt(oMod, "ServerPlayers", nPlayers);
}
All of this relies on SetMovementRate, which can be found in both nwnx_funcs and nwnx_functions (pretty sure the functinos one is deprecated, though).
Like I said, involved, but there's no simpler way that actually works in all cases.
Funky