mahlendur/_work/Data/Scripts/Content/freeAim/config.d
Daniel Heße 892a40ff0f Initial commit with Subversion-codebase from 13/05/2022.
Cleaned up the mess, at this time, only viable data get synced (_work directory for Gothic-data, miranda for mdc-scripts.
Need to symlink files to a working directory.
2022-05-13 17:38:52 +02:00

351 lines
24 KiB
D

/*
* G2 Free Aim v0.1.2 - Free aiming for the video game Gothic 2 by Piranha Bytes
* Copyright (C) 2016 mud-freak (@szapp)
*
* This file is part of G2 Free Aim.
* <http://github.com/szapp/g2freeAim>
*
* G2 Free Aim is free software: you can redistribute it and/or modify
* it under the terms of the MIT License.
* On redistribution this notice must remain intact and all copies must
* identify the original author.
*
* G2 Free Aim is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* MIT License for more details.
*
* You should have received a copy of the MIT License
* along with G2 Free Aim. If not, see <http://opensource.org/licenses/MIT>.
*
*
* Customizability:
* - Show weakspot debug visualization by default: FREEAIM_DEBUG_WEAKSPOT
* - Show trace ray debug visualization by default: FREEAIM_DEBUG_TRACERAY
* - Apply trigger collision fix (disable coll.): FREEAIM_TRIGGER_COLL_FIX
* - Allow freeAim console commands (cheats): FREEAIM_DEBUG_CONSOLE
* - Maximum bow draw time (ms): FREEAIM_DRAWTIME_MAX
* - Disable free aiming for spells (yes/no): FREEAIM_DISABLE_SPELLS
* - Collect and re-use shot projectiles (yes/no): FREEAIM_REUSE_PROJECTILES
* - Projectile instance for re-using: freeAimGetUsedProjectileInstance(instance, targetNpc)
* - Draw force (gravity/drop-off) calculation: freeAimGetDrawForce(weapon, talent)
* - Accuracy calculation: freeAimGetAccuracy(weapon, talent)
* - Reticle style (texture, color, size): freeAimGetReticleRanged(target, weapon, talent, distance)
* - Reticle style for spells: freeAimGetReticleSpell(target, spellID, spellInst, spellLevel, ..)
* - Hit registration on npcs (e.g. friendly-fire): freeAimHitRegNpc(target, weapon, material)
* - Hit registration on world: freeAimHitRegWld(shooter, weapon, material, texture)
* - Change the base damage at time of shooting: freeAimScaleInitialDamage(basePointDamage, weapon, talent)
* - Critical hit calculation (position, damage): freeAimCriticalHitDef(target, weapon, damage)
* - Critical hit event (print, sound, xp, ...): freeAimCriticalHitEvent(target, weapon)
* Advanced (modification not recommended):
* - Scatter radius for accuracy: FREEAIM_SCATTER_DEG
* - Camera view (shoulder view): FREEAIM_CAMERA and FREEAIM_CAMERA_X_SHIFT
* - Max time before projectile drop-off: FREEAIM_TRAJECTORY_ARC_MAX
* - Gravity of projectile after drop-off: FREEAIM_PROJECTILE_GRAVITY
* - Turn speed while aiming: FREEAIM_ROTATION_SCALE
* - Additional hit detection test (EXPERIMENTAL): FREEAIM_HITDETECTION_EXP
* - Shift the aim vob: freeAimShiftAimVob(spellID)
*/
/* Initialize fixed settings. This function is called once at the beginning of each session. Set the constants here */
func void freeAimInitConstants() {
// If you want to change a setting, uncomment the respective line. These are the default values.
FREEAIM_REUSE_PROJECTILES = 0; // Enable collection and re-using of shot projectiles
// FREEAIM_DISABLE_SPELLS = 0; // If true, free aiming is disabled for spells (not for ranged)
// FREEAIM_DRAWTIME_MAX = 1200; // Max draw time (ms): When is the bow fully drawn
// FREEAIM_DEBUG_CONSOLE = 1; // Console commands for debugging. Set to zero in final mod
// FREEAIM_DEBUG_WEAKSPOT = 0; // Visualize weakspot bbox and trajectory by default
// FREEAIM_DEBUG_TRACERAY = 0; // Visualize trace ray bboxes and trajectory by default
// FREEAIM_TRIGGER_COLL_FIX = 1; // Apply trigger collision fix (disable collision)
// Modifying any line below is not recommended!
// FREEAIM_SCATTER_DEG = 2.2; // Maximum scatter radius in degrees
// FREEAIM_TRAJECTORY_ARC_MAX = 400; // Max time (ms) after which the trajectory drops off
// FREEAIM_PROJECTILE_GRAVITY = 0.1; // The gravity decides how fast the projectile drops
// FREEAIM_CAMERA = "CamModFreeAim"; // CCamSys_Def script instance for free aim
// FREEAIM_CAMERA_X_SHIFT = 0; // One, if camera is set to shoulderview, s.a. (not recommended)
// FREEAIM_ROTATION_SCALE = 0.16; // Turn rate. Non-weapon mode is 0.2 (zMouseRotationScale)
// FREEAIM_HITDETECTION_EXP = 0; // Additional hit detection (EXPERIMENTAL)
};
/* Modify this function to alter the draw force calculation. Scaled between 0 and 100 (percent) */
func int freeAimGetDrawForce(var C_Item weapon, var int talent) {
var int drawTime; drawTime = MEM_Timer.totalTime - freeAimBowDrawOnset;
// Possibly incorporate more factors like e.g. a quick-draw talent, weapon-specific stats, ...
if (weapon.flags & ITEM_CROSSBOW) { return 100; }; // Always full draw force on crossbows
// For now the draw time is scaled by a maximum. Replace FREEAIM_DRAWTIME_MAX by a variable for a quick-draw talent
var int drawForce; drawForce = (100 * drawTime) / FREEAIM_DRAWTIME_MAX;
if (drawForce < 0) { drawForce = 0; } else if (drawForce > 100) { drawForce = 100; }; // Respect the ranges
return drawForce;
};
/* Modify this function to alter accuracy calculation. Scaled between 0 and 100 (percent) */
func int freeAimGetAccuracy(var C_Item weapon, var int talent) {
// Add any other factors here e.g. weapon-specific accuracy stats, weapon spread, accuracy talent, ...
// Check if bow or crossbow with (weapon.flags & ITEM_BOW) or (weapon.flags & ITEM_CROSSBOW)
// Here the talent is scaled by draw force: draw force=100% => accuracy=talent; draw force=0% => accuracy=talent/2
var int drawForce; drawForce = freeAimGetDrawForce(weapon, talent); // Already scaled to [0, 100]
var int accuracy; accuracy = (talent-talent/2)*drawForce/100+talent/2;
if (accuracy < 0) { accuracy = 0; } else if (accuracy > 100) { accuracy = 100; }; // Respect the ranges
return accuracy;
};
// This a list of available reticle textures. Some of them are animated as indicated. Animated textures can be passed to
// the following functions:
// reticle.texture = freeAimAnimateReticleByTime(textureFileName, framesPerSecond, numberOfFrames)
// reticle.texture = freeAimAnimateReticleByPercent(textureFileName, 100, numberOfFrames) // Where 100 is a percentage
const string RETICLE_DOT = "RETICLEDOT.TGA";
const string RETICLE_CROSSTWO = "RETICLECROSSTWO.TGA";
const string RETICLE_CROSSTHREE = "RETICLECROSSTHREE.TGA";
const string RETICLE_CROSSFOUR = "RETICLECROSSFOUR.TGA";
const string RETICLE_X = "RETICLEX.TGA";
const string RETICLE_CIRCLE = "RETICLECIRCLE.TGA";
const string RETICLE_CIRCLECROSS = "RETICLECIRCLECROSS.TGA";
const string RETICLE_DOUBLECIRCLE = "RETICLEDOUBLECIRCLE.TGA"; // Can be animated (rotation) 10 Frames [00..09]
const string RETICLE_PEAK = "RETICLEPEAK.TGA";
const string RETICLE_NOTCH = "RETICLENOTCH.TGA"; // Can be animated (expanding) 17 Frames [00..16]
const string RETICLE_TRI_IN = "RETICLETRIIN.TGA"; // Can be animated (expanding) 17 Frames [00..16]
const string RETICLE_TRI_IN_DOT = "RETICLETRIINDOT.TGA"; // Can be animated (expanding) 17 Frames [00..16]
const string RETICLE_TRI_OUT_DOT = "RETICLETRIOUTDOT.TGA"; // Can be animated (expanding) 17 Frames [00..16]
const string RETICLE_DROP = "RETICLEDROP.TGA"; // Can be animated (expanding) 8 Frames [00..07]
const string RETICLE_FRAME = "RETICLEFRAME.TGA";
const string RETICLE_EDGES = "RETICLEEDGES.TGA";
const string RETICLE_BOWL = "RETICLEBOWL.TGA";
const string RETICLE_HORNS = "RETICLEHORNS.TGA";
const string RETICLE_BOLTS = "RETICLEBOLTS.TGA";
const string RETICLE_BLAZE = "RETICLEBLAZE.TGA"; // Can be animated (flames) 10 Frames [00..09]
const string RETICLE_WHIRL = "RETICLEWHIRL.TGA"; // Can be animated (rotation) 10 Frames [00..09]
const string RETICLE_BRUSH = "RETICLEBRUSH.TGA";
const string RETICLE_SPADES = "RETICLESPADES.TGA";
const string RETICLE_SQUIGGLE = "RETICLESQUIGGLE.TGA";
/* Modify this function to alter the reticle texture, color and size (scaled between 0 and 100) for ranged combat. */
func void freeAimGetReticleRanged(var C_Npc target, var C_Item weapon, var int talent, var int dist, var int rtrnPtr) {
var Reticle reticle; reticle = _^(rtrnPtr);
// Color (do not set the color to preserve the original texture color)
if (Hlp_IsValidNpc(target)) { // The argument 'target' might be empty!
var int att; att = Npc_GetAttitude(target, hero);
if (att == ATT_FRIENDLY) { reticle.color = Focusnames_Color_Friendly(); }
else if (att == ATT_NEUTRAL) { reticle.color = Focusnames_Color_Hostile(); }
else if (att == ATT_HOSTILE) { reticle.color = Focusnames_Color_Hostile(); };
};
// Size (scale between [0, 100]: 0 is smallest, 100 is biggest)
reticle.size = -dist + 100; // Inverse aim distance: bigger for closer range: 100 for closest, 0 for most distance
// reticle.size = -freeAimGetDrawForce(weapon, talent) + 100; // Or inverse draw force: bigger for less draw force
// reticle.size = -freeAimGetAccuracy(weapon, talent) + 100; // Or inverse accuracy: bigger with lower accuracy
// More sophisticated customization is also possible: change the texture by draw force, the size by accuracy, ...
if (weapon.flags & ITEM_BOW) { // Change reticle texture by drawforce (irrespective of the reticle size set above)
var int drawForce; drawForce = freeAimGetDrawForce(weapon, talent); // Already scaled between [0, 100]
reticle.texture = freeAimAnimateReticleByPercent(RETICLE_NOTCH, drawForce, 17); // Animate reticle by draw force
} else if (weapon.flags & ITEM_CROSSBOW) { // Change reticle texture by distance
reticle.size = 75; // Keep the size fixed here
reticle.texture = freeAimAnimateReticleByPercent(RETICLE_DROP, dist, 8); // Animate reticle by distance
};
};
/* Modify this function to alter the reticle texture, color and size (scaled between 0 and 100) for magic combat. */
func void freeAimGetReticleSpell(var C_Npc target, var int spellID, var C_Spell spellInst, var int spellLevel,
var int isScroll, var int manaInvested, var int dist, var int rtrnPtr) {
var Reticle reticle; reticle = _^(rtrnPtr);
// 1. Texture (needs to be set, otherwise reticle will not be displayed)
// if (spellInst.spellType == SPELL_GOOD) { reticle.texture = RETICLE_CIRCLECROSS; }
// else if (spellInst.spellType == SPELL_NEUTRAL) { reticle.texture = RETICLE_CIRCLECROSS; }
// else if (spellInst.spellType == SPELL_BAD) { reticle.texture = RETICLE_CIRCLECROSS; };
// 2. Color (do not set the color to preserve the original texture color)
if (Hlp_IsValidNpc(target)) { // The argument 'target' might be empty!
var int att; att = Npc_GetAttitude(target, hero);
if (att == ATT_FRIENDLY) { reticle.color = Focusnames_Color_Friendly(); }
else if (att == ATT_NEUTRAL) { reticle.color = Focusnames_Color_Hostile(); }
else if (att == ATT_HOSTILE) { reticle.color = Focusnames_Color_Hostile(); };
};
// 3. Size (scale between [0, 100]: 0 is smallest, 100 is biggest)
reticle.size = -dist + 100; // Inverse aim distance: bigger for closer range: 100 for closest, 0 for most distance
// More sophisticated customization is also possible: change the texture by spellID, the size by spellLevel, ...
// Size by spell level for invest spells (e.g. increase size by invest level)
// if (spellLevel < 2) { reticle.size = 75; }
// else if (spellLevel >= 2) { reticle.size = 100; };
// Different reticle for scrolls
// if (isScroll) { reticle.color = RGBA(125, 200, 250, 255); }; // Light blue
// Scale size by the amount of mana invested
// reticle.size = manaInvested; // This should be scaled between [0, 100]
// One possibility is to set the reticle texture by grouping the spells, as it is done below
// Ice spells
if (spellID == SPL_Icebolt)
|| (spellID == SPL_IceCube)
|| (spellID == SPL_IceLance) {
reticle.texture = RETICLE_SPADES;
} // Water spells
else if (spellID == SPL_WaterFist)
|| (spellID == SPL_Inflate)
|| (spellID == SPL_Geyser)
|| (spellID == SPL_Waterwall) {
reticle.texture = freeAimAnimateReticleByTime(RETICLE_WHIRL, 30, 10); // Animate reticle with 30 FPS (10 Frames)
} // Fire spells
else if (spellID == SPL_Firebolt)
|| (spellID == SPL_InstantFireball)
|| (spellID == SPL_ChargeFireball)
|| (spellID == SPL_Pyrokinesis)
|| (spellID == SPL_Firestorm) {
reticle.texture = RETICLE_HORNS;
} // Electric spells
else if (spellID == SPL_Zap)
|| (spellID == SPL_LightningFlash)
|| (spellID == SPL_ChargeZap) {
reticle.texture = freeAimAnimateReticleByTime(RETICLE_BLAZE, 15, 10); // Animate reticle with 15 FPS (10 Frames)
} // Paladin spells
else if (spellID == SPL_PalHolyBolt)
|| (spellID == SPL_PalRepelEvil)
|| (spellID == SPL_PalDestroyEvil) {
reticle.texture = RETICLE_FRAME;
} // Evil spells
else if (spellID == SPL_BreathOfDeath)
|| (spellID == SPL_MasterOfDisaster)
|| (spellID == SPL_Energyball)
|| (spellID == SPL_Skull) {
reticle.texture = RETICLE_BOWL;
} else {
reticle.texture = RETICLE_EDGES; // Set this as "default" texture here (if none of the conditions above is met)
};
// Blink (mud-freak 2016)
if (spellID == SPL_Blink) { reticle.texture = ""; }; // No reticle for blink
};
/* Modify this function to disable hit registration on npcs, e.g. 'ineffective' ranged weapons, no friendly-fire, ... */
func int freeAimHitRegNpc(var C_Npc target, var C_Item weapon, var int material) {
// Valid return values are:
const int DESTROY = 0; // No hit reg (no damage), projectile is destroyed
const int COLLIDE = 1; // Hit reg (damage), projectile is put into inventory
const int DEFLECT = 2; // No hit reg (no damage), projectile is repelled
// The argument 'material' holds the material of the armor (of the target), -1 for no armor equipped
// For armors of npcs the materials are defined as in Constants.d (MAT_METAL, MAT_WOOD, ...)
if (target.aivar[AIV_PARTYMEMBER]) // Disable friendly-fire
&& (target.aivar[AIV_LASTTARGET] != Hlp_GetInstanceID(hero)) { return DESTROY; };
// if (material == MAT_METAL) && (Hlp_Random(100) < 20) { return DEFLECT; }; // Metal armors may be more durable
// The weapon can also be considered (e.g. ineffective weapons). Make use of 'weapon' for that
// Caution: Weapon may have been unequipped already at this time (unlikely)! Use Hlp_IsValidItem(weapon)
// if (Hlp_IsValidItem(weapon)) && (weapon.ineffective) { return DEFLECT; }; // Special case for weapon property
return COLLIDE; // Usually all shots on npcs should be registered, see freeAimGetAccuracy() above
};
/* Modify this function to disable hit registration on the world, e.g. deflection of metal, stuck in wood, ... */
func int freeAimHitRegWld(var C_Npc shooter, var C_Item weapon, var int material, var string texture) {
// This function, unlike freeAimHitRegNpc() and all other functions here, is also called for npc shooters!
// Valid return values are:
const int DESTROY = 0; // Projectile is destroyed on impact
const int COLLIDE = 1; // Projectile gets stuck in the surface
const int DEFLECT = 2; // Projectile is repelled
// Note: The materials of the world are defined differently (than the familiar item-materials):
const int METAL = 1;
const int STONE = 2;
const int WOOD = 3;
const int EARTH = 4;
const int WATER = 5;
const int SNOW = 6;
const int UNDEF = 0;
if (material == WOOD) { return COLLIDE; }; // Projectiles stay stuck in wood (default in gothic)
if (Hlp_StrCmp(texture, "MOWOBOWMARK01.TGA")) { return COLLIDE; }; // Condition by surface texture
// if (Npc_IsPlayer(shooter)) ... // Keep in mind that this function is also called for npc shooters
if (material == STONE) && (Hlp_Random(100) < 20) { return DESTROY; }; // The projectile might break on impact
// The example in the previous line can also be treated in freeAimGetUsedProjectileInstance() below
// The weapon can also be considered (e.g. ineffective weapons). Make use of 'weapon' for that
// Caution: Weapon may have been unequipped already at this time (unlikely)! Use Hlp_IsValidItem(weapon)
// if (Hlp_IsValidItem(weapon)) && (weapon.ineffective) { return DEFLECT; }; // Special case for weapon property
return DEFLECT; // Projectiles deflect off of all other surfaces
};
/* Modify this function to alter the base damage of projectiles at time of shooting (only DAM_POINT) */
func int freeAimScaleInitialDamage(var int basePointDamage, var C_Item weapon, var int talent) {
// This function should not be necessary, all damage specifications should be set in the item scripts. However,
// here the initial damage (DAM_POINT) may be scaled by draw force, accuracy, ...
// Check if bow or crossbow with (weapon.flags & ITEM_BOW) or (weapon.flags & ITEM_CROSSBOW)
// Here the damage is scaled by draw force: draw force=100% => baseDamage; draw force=0% => baseDamage/2
var int drawForce; drawForce = freeAimGetDrawForce(weapon, talent); // Already scaled to [0, 100]
drawForce = 50 * drawForce / 100 + 50; // Re-scale the drawforce to [50, 100]
return (basePointDamage * drawForce) / 100; // Scale initial point damage by draw force
};
/* Modify this function to define a critical hit by weak spot (e.g. head node for headshot), its size and the damage */
func void freeAimCriticalHitDef(var C_Npc target, var C_Item weapon, var int damage, var int rtrnPtr) {
var Weakspot weakspot; weakspot = _^(rtrnPtr);
// This function is dynamic: It is called on every hit and the weakspot and damage can be calculated individually
// Possibly incorporate weapon-specific stats, headshot talent, dependency on target, ...
// The damage may depend on the target npc (e.g. different damage for monsters). Make use of 'target' argument
// if (target.guild < GIL_SEPERATOR_HUM) { }; // E.g. special case for humans
// The weapon can also be considered (e.g. weapon specific damage). Make use of 'weapon' for that
// Caution: Weapon may have been unequipped already at this time (unlikely)! Use Hlp_IsValidItem(weapon) to check
// if (Hlp_IsValidItem(weapon)) && (weapon.certainProperty > 10) { }; // E.g. special case for weapon property
// The damage is a float and represents the new base damage (damage of weapon), not the final damage!
if (target.guild < GIL_SEPERATOR_HUM) { // Humans: head shot
weakspot.node = "Bip01 Head"; // Upper/lower case is not important, but spelling and spaces are
weakspot.dimX = -1; // Retrieve from model (works only on humans and only for head node!)
weakspot.dimY = -1;
weakspot.bDmg = mulf(damage, castToIntf(2.0)); // Double the base damage. This is a float
// } else if (target.aivar[AIV_MM_REAL_ID] == ID_TROLL) {
// weakspot.node = "Bip01 R Finger0"; // Difficult to hit when the troll attacks
// weakspot.dimX = 100; // 100x100cm size
// weakspot.dimY = 100;
// weakspot.bDmg = mulf(damage, castToIntf(1.75));
// } else if (target.aivar[AIV_MM_REAL_ID] == ...
// ...
} else if (target.guild == GIL_BLOODFLY) || (target.guild == GIL_MEATBUG) { // Models that don't have a head node
weakspot.node = ""; // Disable critical hits this way
} else { // Default
weakspot.node = "Bip01 Head";
weakspot.dimX = 50; // 50x50cm size
weakspot.dimY = 50;
weakspot.bDmg = mulf(damage, castToIntf(2.0)); // Double the base damage. This is a float
};
};
/* Use this function to create an event when getting a critical hit, e.g. print or sound jingle, leave blank for none */
func void freeAimCriticalHitEvent(var C_Npc target, var C_Item weapon) {
// The event may depend on the target npc (e.g. different sound for monsters). Make use of 'target' argument
// if (target.guild < GIL_SEPERATOR_HUM) { }; // E.g. special case for humans
// The critical hits could also be counted here to give an xp reward after 25 headshots
// The weapon can also be considered (e.g. weapon specific print). Make use of 'weapon' for that
// Caution: Weapon may have been unequipped already at this time (unlikely)! Use Hlp_IsValidItem(weapon) to check
// if (Hlp_IsValidItem(weapon)) && (weapon.certainProperty > 10) { }; // E.g. special case for weapon property
// Simple screen notification
// PrintS("Critical hit");
// Shooter-like hit marker
var int hitmark;
if (!Hlp_IsValidHandle(hitmark)) { // Create hitmark if it does not exist
var zCView screen; screen = _^(MEM_Game._zCSession_viewport);
hitmark = View_CreateCenterPxl(screen.psizex/2, screen.psizey/2, 64, 64);
View_SetTexture(hitmark, freeAimAnimateReticleByPercent(RETICLE_TRI_IN, 100, 7)); // Retrieve 7th frame of ani
};
View_Open(hitmark);
FF_ApplyExtData(View_Close, 300, 1, hitmark);
// Sound notification
Snd_Play3D(target, "FREEAIM_CRITICALHIT");
};
/* Modify this function to exchange (or remove) the projectile after shooting for re-using, e.g. used arrow */
func int freeAimGetUsedProjectileInstance(var int projectileInst, var C_Npc inventoryNpc) {
// By returning zero, the projectile is completely removed (e.g. retrieve-projectile-talent not learned yet)
// The argument inventoryNpc holds the npc in whose inventory it will be put, or is empty if it landed in the world
// if (projectileInst == Hlp_GetInstanceID(ItRw_Arrow)) { // Exchange the instance for a "used" one
// if (!Hlp_IsValidItem(ItRw_UsedArrow)) { Wld_InsertItem(ItRw_UsedArrow, MEM_FARFARAWAY); }; // Initialize!
// projectileInst = Hlp_GetInstanceID(ItRw_UsedArrow);
// };
if (Hlp_IsValidNpc(inventoryNpc)) { // Projectile hit npc and will be put into their inventory
if (Npc_IsPlayer(inventoryNpc)) { return 0; }; // Do not put projectiles in player inventory
// if (inventoryNpc.guild < GIL_SEPERATOR_HUM) { return 0; }; // Remove projectile when it hits humans
// if (PLAYER_TALENT_TAKEANIMALTROPHY[REUSE_Arrow] == FALSE) { return 0; }; // Retrieve-projectile-talent
// if (!Npc_HasItems(hero, ItMi_ArrowTool)) { return 0; }; // Player needs tool to remove the projectile
// if (Hlp_Random(100) < 50) { return 0; }; // Chance of retrieval
return projectileInst; // For now it is just preserved (is put in the inventory as is)
} else { // Projectile did not hit npc and landed in world
// if (PLAYER_TALENT_REUSE_ARROW == FALSE) { return 0; }; // Reuse-projectile-talent
return projectileInst; // For now it is just preserved (leave it in the world as is)
};
};
/* Shift the aimvob along the camera out vector for spells (if you don't know what this is, you don't need it) */
func int freeAimShiftAimVob(var int spellID) {
// Blink (mud-freak 2016)
if (spellID == SPL_Blink) { return -100; }; // SPL_Blink visualizes the aim vob: push it away from any wall
// if (spellID == ...) { return -100; }; // Push the aim vob 100 cm away from any wall
return 0;
};