PreciseCrosshair
Table of Contents
- 1. About
- 2. Changelog
- 3. License
- 4. Menus
- 5. Options
- 6. External interface
- 7. Additional crosshairs
- 8. Source
- 8.1. Event handler
- 8.1.1. World tick
- 8.1.2. Render overlay
- 8.1.3. Initialize
- 8.1.4. Prepare projection
- 8.1.5. Load target
- 8.1.6. Draw crosshair
- 8.1.7. Get healths
- 8.1.8. Get aim target
- 8.1.9. Make new crosshair color
- 8.1.10. Make old crosshair color
- 8.1.11. Make draw position
- 8.1.12. Load crosshair
- 8.1.13. Check texture
- 8.1.14. Set external y
- 8.1.15. Set external scale
- 8.1.16. Get default health
- 8.1.17. Disabled on slot 1
- 8.1.18. Disabled when not ready
- 8.1.19. Disabled on no weapon
- 8.1.20. Disabled on no target
- 8.1.21. Is slot 1?
- 8.1.22. Is weapon ready?
- 8.1.23. Is no weapon?
- 8.1.24. HSV to RGB
- 8.1.25. Linear map
- 8.1.26. Get distance scale
- 8.2. Puff
- 8.3. Settings
- 8.1. Event handler
- 9. Tests
{
"old_version": "1.6.0",
"depends": { "libeye": "pc_" }
}
1. About
PreciseCrosshair tries to put a crosshair where you actually shoot.
PreciseCrosshair is a part of DoomToolbox.
How to use
- HUD Options -> Crosshair must be OFF.
- HUD Options -> Default crosshair must be not "None".
Features
- shows where you shoot
- respects autoaim
- supports custom crosshairs
- respects engine crosshair options
- options to mirror crosshairs
- options to disable crosshair on slot 1 and when weapon isn't ready
- option to show target health
Known issues
- PreciseCrosshair can't be precise for custom weapons that shoot from an angle
- Crosshair size may be off on UZDoom/LZDoom before v5.0.0
- "Inverted" crosshair color isn't supported yet
Credits
- Color calculation and crosshair code is taken from GZDoom/UZDoom.
2. Changelog
- added option to disable crosshair when there is no target
v1.6.0:
- moved to the new translation system
- fixed crosshair size for UZDoom 5.0.0
- added new color formula
v1.5.0:
- added writing of result to external cvar
- fixed GetDistanceScale was missing a return
- added distance-based crosshair scaling option
- added options menu icon
- hide crosshair when looking through a camera
- fix ready state detection
- try to respect
A_SetCrosshair(-1)
v1.4.1:
- update Readme
- fix disabled crosshair on certain weapons
- move menu strings to language lump
- add Pixel crosshair
- update Readme
v1.4:
- fix weirdness with max distance
- make crosshair y position change slightly smoother
- update Readme
- add option to color crosshair depending on target health
- port Enhanced crosshair health color from GZDoom
- fix aim when crouching
- update build script
v1.3:
- add option to hide crosshair when no weapon is selected/weapon is holstered
- add minimalist crosshairs
v1.2.3:
- fix menu
- update Readme
v1.2.2:
- fix ZScript version
- update Readme
- add GZDoom version to Readme
- improve weapon ready detection
- update menudef
- update Readme
v1.2.1:
- bump minimal ZScript version
- add links to Readme
- update LibEye
- fix memory leak on game loading
v1.2:
- add options to disable precise crosshair on slot 1 and when weapon isn't ready
- add options to flip the crosshair horizontally and vertically
- remove debug output
- update Readme
- add a cvar with Y position for other mods to read
- add an on/off switch
v1.1:
- added prefix to libeye to avoid conflicts with other mods
- update libeye to support different screen sizes
- add license file
v1.0: initial release
3. License
SPDX-FileCopyrightText: © 2019 Alexander Kromm (m8f) <mmaulwurff@gmail.com> SPDX-License-Identifier: GPL-3.0-only
4. Menus
[enu default] PC_TITLE = "PreciseCrosshair \ck⚀";
AddOptionMenu OptionsMenu { Submenu "$PC_TITLE", pc_Menu } AddOptionMenu OptionsMenuSimple { Submenu "$PC_TITLE", pc_Menu } OptionMenu pc_Menu { Title "PreciseCrosshair" StaticText "$PC_ENGINE" , 1 StaticText "" StaticText "$PC_OFF" Option "$HUDMNU_CROSSHAIRON" , crosshairon, OnOff StaticText "" StaticText "$PC_NONE" Option "$HUDMNU_CROSSHAIR" , crosshair, Crosshairs StaticText "" StaticText "$PC_OTHER" Submenu "$OPTMNU_HUD" , HUDOptions StaticText "" StaticText "$PC_OPTIONS" , 1 StaticText "" Option "$PC_ENABLE" , pc_enable , YesNo StaticText "" Option "$PC_MIRROR_H" , pc_flip_x , YesNo Option "$PC_MIRROR_V" , pc_flip_y , YesNo StaticText "" Option "$PC_SLOT_1" , pc_disable_slot_1 , YesNo Option "$PC_NOT_READY" , pc_disable_not_ready , YesNo Option "$PC_NO_WEAPON" , pc_disable_no_weapon , YesNo Option "$PC_NO_TARGET" , pc_disable_no_target , YesNo Option "$PC_DISTANCE_SCALE" , pc_distance_scale , YesNo StaticText "" Option "$PC_NEW_COLOR_FORMULA", pc_new_color_formula, YesNo Option "$PC_TARGET_COLOR" , pc_target_health , YesNo }
5. Options
server bool pc_enable = true; user bool pc_flip_y = false; user bool pc_flip_x = false; user bool pc_disable_slot_1 = false; user bool pc_disable_not_ready = false; user bool pc_disable_no_weapon = true; user bool pc_disable_no_target = false; user bool pc_distance_scale = true; user bool pc_target_health = false; user bool pc_new_color_formula = true;
6. External interface
user float pc_y; user float pc_scale;
7. Additional crosshairs
97 "$PC_PIXEL" 98 "$PC_BIG" 99 "$PC_SMALL"
8. Source
8.1. Event handler
class pc_EventHandler : EventHandler { private Vector3 _targetPos; private double _distToTarget; private bool _hasTargetPos; private ui int _crosshairNum; private ui bool _isCrossExisting; private ui TextureID _crosshairTexture; private transient bool _isInitialized; private transient bool _isPrepared; private transient Cvar _cvarRenderer; private transient Cvar _externalY; private transient Cvar _externalScale; private pc_ProjScreen _projection; private pc_GlScreen _glProjection; private pc_SwScreen _swProjection; private pc_Settings _settings; private DynamicValueInterpolator _yPositionInterpolator; private Cvar _cvarCrosshairColorFull; private Cvar _cvarCrosshairColorMax; }
8.1.1. World tick
override void WorldTick() { if (!_isInitialized) { initialize(); } prepareProjection(); }
8.1.2. Render overlay
override void RenderOverlay(RenderEvent event) { if (!_isInitialized || !_isPrepared) { return; } PlayerInfo player = players[consolePlayer]; if (player == NULL) { return; } loadTarget(event.viewAngle, event.viewPitch); loadCrosshair(player); drawCrosshair(player, event); }
8.1.3. Initialize
private void initialize() { PlayerInfo player = players[consolePlayer]; _glProjection = new("pc_GlScreen"); _swProjection = new("pc_SwScreen"); _cvarRenderer = Cvar.GetCvar("vid_rendermode", player); _settings = new("pc_Settings").init(player); _yPositionInterpolator = DynamicValueInterpolator.Create(Screen.GetHeight() / 2, 0.5, 1, 1000000); _cvarCrosshairColorFull = Cvar.getCvar("crosshairColorFull", player); _cvarCrosshairColorMax = Cvar.getCvar("crosshairColorMax", player); _isInitialized = true; }
8.1.4. Prepare projection
private void prepareProjection () { if(_cvarRenderer) { switch (_cvarRenderer.GetInt()) { default: _projection = _glProjection; break; case 0: case 1: _projection = _swProjection; break; } } else { Console.printf("WARNING: cannot get render mode"); _projection = _glProjection; } _isPrepared = (_projection != NULL); }
8.1.5. Load target
private void loadTarget(double angle, double pitch) const { PlayerInfo p = players[consolePlayer]; PlayerPawn a = p.mo; pitch = a.AimTarget() ? a.BulletSlope(NULL, ALF_PORTALRESTRICT) : pitch; FLineTraceData data; double hitHeight = a.height / 2 + a.AttackZOffset * p.crouchFactor; _hasTargetPos = a.LineTrace(angle, 4000.0, pitch, 0, hitHeight, 0, 0, data); if (_hasTargetPos) { _targetPos = data.hitlocation; _distToTarget = data.distance; } }
8.1.6. Draw crosshair
private ui void drawCrosshair(PlayerInfo player, RenderEvent event) { Vector2 drawPos = makeDrawPos(player, event); setExternalY(player, drawPos.y); if ( !_isCrossExisting || gamestate == GS_TITLELEVEL || player.mo.health <= 0 || automapactive || !pc_enable || disabledOnSlot1(player) || disabledWhenNotReady(player) || disabledOnNoWeapon(player) || event.camera != player.mo || disabledOnNoTarget(player) ) { return; } int screenWidth = Screen.GetWidth(); int screenHeight = Screen.GetHeight(); int mult = max(screenHeight / 720, 1); // 1080p: 1, 1440p: 2, 4K: 3 double size = ((crosshairscale > 0.0f) ? crosshairscale : 1.0) * mult; if (crosshairgrow) size *= StatusBar.crosshairSize; vector2 textureSize = TexMan.getScaledSize(_crosshairTexture) * size; double width = round(textureSize.x); double height = round(textureSize.y); if (_settings.isScaledByDistance()) { double distFac = GetDistanceScale(0, 512); width *= distFac; height *= distFac; } let [hasHealth, health] = getHealths(player); bool isNewColorFormula = _settings.isNewColorFormula(); int crossColor = !hasHealth ? crosshaircolor : isNewColorFormula ? makeNewCrosshairColor(health) : makeOldCrosshairColor(health); _yPositionInterpolator.Update(int(drawPos.y)); Screen.DrawTexture( _crosshairTexture , false , screenWidth / 2 , _yPositionInterpolator.GetValue() , DTA_DestWidthF , width , DTA_DestHeightF , height , DTA_AlphaChannel , true , DTA_KeepRatio , true , DTA_FillColor , crossColor & 0xFFFFFF , DTA_FlipX , _settings.isFlipX() , DTA_FlipY , _settings.isFlipY() ); }
8.1.7. Get healths
// Returns noHealth flag, current health (scaled to 0-100). private ui bool, int getHealths(PlayerInfo player) { if (!_settings.isTargetHealth()) return true, scale(player.health, 100, getDefaultHealth(player)); let aimTarget = getAimTarget(player.mo); if (aimTarget) return true, scale(aimTarget.health, 100, aimTarget.getSpawnHealth()); else return false, 0; } private ui static int scale(int value, int scaleMax, int valueMax) { return value * scaleMax / valueMax; }
8.1.8. Get aim target
private play Actor getAimTarget(Actor a) const { return a.AimTarget(); }
8.1.9. Make new crosshair color
// Color functions are adapted from UZDoom/src/utility/colorspace.cpp private ui Color makeNewCrosshairColor(int health) { if (crosshairhealth == 0) return crosshaircolor; if (crosshairhealth == 3) return 0xffffff; if (_cvarCrosshairColorFull == NULL || _cvarCrosshairColorMax == NULL) return makeOldCrosshairColor(health); int health = clamp(health, 0, (crosshairhealth == 1) ? 100 : 200); int hi = _cvarCrosshairColorFull.getInt(); int lo = crosshaircolor; double mix = 1.0; if (health > 100) { lo = hi; hi = _cvarCrosshairColorMax.getInt(); mix = (health - 100) / 100.0; } else if (health <= 85) { mix = health / 85.0; } let c = mixColor(lo, hi, mix); return Color(c.r, c.g, c.b); } private static ui Color mixColor(Color a, Color b, double mix) { if (mix <= 0) return a; if (mix >= 1) return b; let [al, ac, ah] = rgbToOklch(a); let [bl, bc, bh] = rgbToOklch(b); double l = lerp(al, bl, mix); double c = lerp(ac, bc, mix); double h = alerp(ah, bh, mix); return oklchToRgb(l, c, h); } private static ui double, double, double rgbToOklch(Color a) { let [l, a, b] = rgbToOklab(a); let [c, h] = oklabToOklch(a, b); return l, c, h; } private static ui double, double, double rgbToOklab(Color a) { double r = linear(a.r / 255.0); double g = linear(a.g / 255.0); double b = linear(a.b / 255.0); double l = (0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b) ** (1.0 / 3); double m = (0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b) ** (1.0 / 3); double s = (0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b) ** (1.0 / 3); return (l * +0.2104542553 + m * +0.7936177850 + s * -0.0040720468), (l * +1.9779984951 + m * -2.4285922050 + s * +0.4505937099), (l * +0.0259040371 + m * +0.7827717662 + s * -0.8086757660); } private static ui double linear(double c) { return (c >= 0.04045) ? (((c + 0.055) / 1.055) ** 2.4) : (c / 12.92); } private static ui double, double oklabToOklch(double a, double b) { double c = sqrt(a * a + b * b); if (c < 0.0001) return c, double.NAN; double h = atan2(b, a); if (h >= 0) return c, h; return c, h + 360; } private static ui Color oklchToRgb(double l, double c, double h) { let [a, b] = oklchToOklab(c, h); return oklabToRgb(l, a, b); } private static ui double, double oklchToOklab(double c, double h) { double a = (h == double.NAN) ? 0 : c * cos(h); double b = (h == double.NAN) ? 0 : c * sin(h); return a, b; } private static ui Color oklabToRgb(double l, double a, double b) { double l2 = (L + a * +0.3963377774 + b * +0.2158037573) ** 3; double m = (L + a * -0.1055613458 + b * -0.0638541728) ** 3; double s = (L + a * -0.0894841775 + b * -1.2914855480) ** 3; double r = gamma(l2 * +4.0767416621 + m * -3.3077115913 + s * +0.2309699292); double g = gamma(l2 * -1.2684380046 + m * +2.6097574011 + s * -0.3413193965); double b = gamma(l2 * -0.0041960863 + m * -0.7034186147 + s * +1.7076147010); return Color(int(255 * r), int(255 * g), int(255 * b)); } private static ui double gamma(double c) { return clamp(((c >= 0.0031308) ? (1.055 * (c ** (1 / 2.4)) - 0.055) : (12.92 * c)), 0.0, 1.0); } private static ui double lerp(double a, double b, double t) { // Naive, good enough. return a + t * (b - a); } private static ui double alerp(double a, double b, double t) { double delta = (b - a) % 360; if (delta > 180) delta -= 360; if (delta < -180) delta += 360; return (a + delta * t + 360) % 360; }
8.1.10. Make old crosshair color
private static ui Color makeOldCrosshairColor(int health) { if (crosshairhealth == 1) { // "Standard" crosshair health (green-red) if (health >= 85) { return 0x00ff00; } else { int red; int green; health -= 25; if (health < 0) { health = 0; } if (health < 30) { red = 255; green = health * 255 / 30; } else { red = (60 - health) * 255 / 30; green = 255; } return Color(red, green, 0); } } else if (crosshairhealth == 2) { // "Enhanced" crosshair health (blue-green-yellow-red) int health = clamp(health, 0, 200); double rr; double gg; double bb; double saturation = health < 150 ? 1.0 : 1.0 - (health - 150) / 100.0; HSVtoRGB(rr, gg, bb, health * 1.2f, saturation, 1); int red = int(rr * 255); int green = int(gg * 255); int blue = int(bb * 255); return Color(red, green, blue); } return crosshaircolor; }
8.1.11. Make draw position
private ui Vector2 makeDrawPos(PlayerInfo player, RenderEvent event) { if (!_hasTargetPos) { int x, y, width, height; [x, y, width, height] = Screen.GetViewWindow(); int screenHeight = Screen.GetHeight(); int statusBarHeight = screenHeight - height - x; return (Screen.GetWidth() / 2, (Screen.GetHeight() - statusBarHeight) / 2); } _projection.CacheResolution(); _projection.CacheFov(player.fov); _projection.OrientForRenderOverlay(event); _projection.BeginProjection(); _projection.ProjectWorldPos(_targetPos); pc_Viewport viewport; viewport.FromHud(); Vector2 drawPos = viewport.SceneToWindow(_projection.ProjectToNormal()); return drawPos; }
8.1.12. Load crosshair
private ui void loadCrosshair(PlayerInfo player) { int num = 0; if (!crosshairforce && NULL != player.camera && NULL != player.camera.player && NULL != player.camera.player.readyWeapon) { num = player.camera.player.readyWeapon.crosshair; } if (num == 0) { num = crosshair; } if (_crosshairnum == num && _isCrossExisting) { return; } if (num <= 0) { _crosshairNum = 0; _isCrossExisting = false; return; } string size = (Screen.GetWidth() < 640) ? "S" : "B"; string texName = String.Format("XHAIR%s%d", size, num); TextureID texId = checkTexture(texName); if (!texId.isValid()) { texName = String.Format("XHAIR%s1", size); texId = checkTexture(texName); if (!texId.isValid()) { texId = checkTexture("XHAIRS1"); } } _crosshairNum = num; _crosshairTexture = texId; _isCrossExisting = true; }
8.1.13. Check texture
private ui TextureId checkTexture(string name) { return TexMan.checkForTexture(name, TexMan.Type_MiscPatch, TexMan.TryAny | TexMan.ShortNameOnly); }
8.1.14. Set external y
private play void setExternalY(PlayerInfo player, double y) const { if (_externalY == NULL) _externalY = Cvar.GetCVar("pc_y", player); _externalY.SetFloat(y); }
8.1.15. Set external scale
private play void setExternalScale (PlayerInfo player, double scale) const { if (_externalScale == NULL) _externalScale = Cvar.GetCVar("pc_scale", player); _externalScale.SetFloat(scale); }
8.1.16. Get default health
private ui static int getDefaultHealth(PlayerInfo player) { class<PlayerPawn> type = player.mo.GetClassName(); let default = GetDefaultByType(type); return default.health; }
8.1.17. Disabled on slot 1
private play bool disabledOnSlot1(PlayerInfo player) const { return _settings.isDisabledOnSlot1() && isSlot1(player); }
8.1.18. Disabled when not ready
private play bool disabledWhenNotReady(PlayerInfo player) const { return _settings.isDisabledOnNotReady() && !isWeaponReady(player); }
8.1.19. Disabled on no weapon
private play bool disabledOnNoWeapon(PlayerInfo player) const { return _settings.isDisabledOnNoWeapon() && isNoWeapon(player); }
8.1.20. Disabled on no target
private play bool disabledOnNoTarget(PlayerInfo player) const { return _settings.isDisabledOnNoTarget() && (getAimTarget(player.mo) == NULL); }
8.1.21. Is slot 1?
private static bool isSlot1(PlayerInfo player) { Weapon w = player.readyWeapon; if (w == NULL) { return false; } int located; int slot; [located, slot] = player.weapons.LocateWeapon(w.GetClassName()); bool slot1 = (slot == 1); return slot1; }
8.1.22. Is weapon ready?
private static bool isWeaponReady(PlayerInfo player) { bool isReady = player.WeaponState & WF_WEAPONREADY || player.WeaponState & WF_WEAPONREADYALT || player.WeaponState & WF_WEAPONZOOMOK || player.WeaponState & WF_WEAPONSWITCHOK || player.cmd.buttons & BT_ATTACK || player.cmd.buttons & BT_ALTATTACK; return isReady; }
8.1.23. Is no weapon?
private static bool isNoWeapon(PlayerInfo player) { Weapon w = player.readyWeapon; bool isNo = (w == NULL) || (w.GetClassName() == "m8f_wm_Holstered"); return isNo; }
8.1.24. HSV to RGB
private static ui void HSVtoRGB(out double r, out double g, out double b, double h, double s, double v) { int i; double f, p, q, t; if (s == 0) { // achromatic (grey) r = g = b = v; return; } h /= 60; // sector 0 to 5 i = int(floor(h)); f = h - i; // factorial part of h p = v * (1 - s); q = v * (1 - s * f); t = v * (1 - s * (1 - f)); switch (i) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; default: r = v; g = p; b = q; break; } }
8.1.25. Linear map
private static clearscope double linearMap(double val, double source_min, double source_max, double out_min, double out_max) { return (val - source_min) * (out_max - out_min) / (source_max - source_min) + out_min; }
8.1.26. Get distance scale
private ui double getDistanceScale(double minDist = 0, double maxDist = 1024, double minScale = 1.0, double maxScale = 4.0) { double scale = minScale; if (_distToTarget < maxDist) { double dist = Clamp(_distToTarget, minDist, maxDist); scale = LinearMap(dist, maxdist, mindist, minscale, maxscale); } setExternalScale(players[consoleplayer], scale); return scale; }
8.2. Puff
class pc_Puff : Actor { Default { Mass 0; Radius 1; Height 2; +NOBLOCKMAP +NOGRAVITY +BLOODLESSIMPACT +PUFFONACTORS +DONTSPLASH +NOTRIGGER +NOTONAUTOMAP +SKYEXPLODE } States { Spawn: TNT1 A 0; Stop; } }
8.3. Settings
// This class represents a single boolean setting. class pc_BoolSetting : pc_CvarSetting { bool value() { return variable().GetInt(); } pc_BoolSetting init(string cvarName, PlayerInfo p) { super.init(cvarName, p); return self; } }
/// This class represents a single setting. class pc_CvarSetting : pc_SettingsBase { pc_CvarSetting init(string cvarName, PlayerInfo p) { _cvar = CVar.GetCvar(cvarName, p); return self; } override void resetCvarsToDefaults() { _cvar.ResetToDefault(); } protected Cvar variable() { return _cvar; } private transient CVar _cvar; }
class pc_Settings : pc_SettingsPack { pc_Settings init(PlayerInfo player) { _player = player; return self; } bool isFlipX() { checkInit(); return _flipX.value(); } bool isFlipY() { checkInit(); return _flipY.value(); } bool isDisabledOnSlot1 () { checkInit(); return _disableOnSlot1 .value(); } bool isDisabledOnNotReady() { checkInit(); return _disableOnNotReady.value(); } bool isDisabledOnNoWeapon() { checkInit(); return _disableNoWeapon .value(); } bool isDisabledOnNoTarget() { checkInit(); return _disableOnNoTarget.value(); } bool isScaledByDistance () { checkInit(); return _distanceScale .value(); } bool isTargetHealth() { checkInit(); return _targetHealth.value(); } bool isNewColorFormula() { checkInit(); return _newColorFormula.value(); } private void checkInit() { if (_isInitialized) { return; } clear(); push(_flipX = newBoolSetting("pc_flip_x" )); push(_flipY = newBoolSetting("pc_flip_y" )); push(_disableOnSlot1 = newBoolSetting("pc_disable_slot_1" )); push(_disableOnNotReady = newBoolSetting("pc_disable_not_ready")); push(_disableNoWeapon = newBoolSetting("pc_disable_no_weapon")); push(_disableOnNoTarget = newBoolSetting("pc_disable_no_target")); push(_targetHealth = newBoolSetting("pc_target_health" )); push(_newColorFormula = newBoolSetting("pc_new_color_formula")); push(_distanceScale = newBoolSetting("pc_distance_scale" )); _isInitialized = true; } private pc_BoolSetting newBoolSetting(String name) { return new("pc_BoolSetting").init(name, _player); } private pc_BoolSetting _flipX; private pc_BoolSetting _flipY; private pc_BoolSetting _disableOnSlot1; private pc_BoolSetting _disableOnNotReady; private pc_BoolSetting _disableNoWeapon; private pc_BoolSetting _disableOnNoTarget; private pc_BoolSetting _targetHealth; private pc_BoolSetting _newColorFormula; private pc_BoolSetting _distanceScale; private PlayerInfo _player; private transient bool _isInitialized; }
// This class provides the base functions for various settings classes. class pc_SettingsBase abstract { // Reset CVars to their default values. abstract void resetCvarsToDefaults(); }
// This class represents a pack of settings. class pc_SettingsPack : pc_SettingsBase { override void resetCvarsToDefaults() { int nSettings = _settings.size(); for (int i = 0; i < nSettings; ++i) { _settings[i].resetCvarsToDefaults(); } } protected void push(pc_SettingsBase setting) { _settings.push(setting); } protected void clear() { _settings.clear(); } private Array<pc_SettingsBase> _settings; }
9. Tests
version 4.14.3
class TestPistol : Pistol replaces Pistol
{
States
{
Ready:
PISG A 0 A_SetCrosshair(1);
PISG A 1 A_WeaponReady();
Loop;
Fire:
PISG B 1 A_SetCrosshair(-1);
PISG C 1 A_Refire();
Goto Ready;
}
}
Check that the add-on at least can be loaded and doesn't error out on a target.
wait 2; map map01 wait 4; summon doomimp wait 8; quit