LazyPoints
Table of Contents
- 1. Source
- 1.1. Parameters
- 1.2. Dispatcher
- 1.3. Bonus View
- 1.4. Counter
- 1.5. Font User
- 1.6. Health Bonus
- 1.7. MapScore
- 1.8. Map Score Item
- 1.9. Option Menu Score Item
- 1.10. PlayerScore
- 1.11. ScoreStorage
- 1.12. Spawner
- 1.13. StaticEventHandler
- 1.14. Timer
- 1.15. TimerBonus
- 1.16. TimerView
- 1.17. Top
- 1.18. TopHintView
- 1.19. View
- 2. Tests
LazyPoints module version: 0.6.5 LazyPoints is a part of DoomToolbox: https://github.com/mmaulwurff/doom-toolbox/ Point scoring for GZDoom. Do things, get points! License: SPDX-FileCopyrightText: © 2025 Alexander Kromm <mmaulwurff@gmail.com> SPDX-License-Identifier: BSD-3-Clause Scoring: - Damage: equal to damage amount. - Telefrag: equal to enemy starting health. - Kills: 10% of starting health of killed enemy; - Chain kills: 10 points for each enemy killed in 3 seconds after previous kill. - Secrets: 250 points. - Map items: 5 points. - Keys: 250 points. - Barrels: 5 points. - Player with 50% and more health gets kill points (with chain kill bonus) multiplied by 1.5, with 100% and more - by 2. Scoring system is based on ScoreDoom: https://zdoom.org/wiki/ScoreDoom. To use (besides ZScript code): - define cvars: nosave string NAMESPACE_score = ""; user bool NAMESPACE_show = true; server string NAMESPACE_parameters_class = "NAMESPACE_Parameters"; - add event handlers in mapinfo: GameInfo { EventHandlers = "NAMESPACE_Dispatcher", "NAMESPACE_StaticView" } - add access to top scores menu (add it to a menu or add a key). Menudef: OptionMenu NAMESPACE_TopMenu { class NAMESPACE_Top Title "Top Points" }
nosave string NAMESPACE_score = ""; user bool NAMESPACE_show = true; server string NAMESPACE_parameters_class = "NAMESPACE_Parameters";
GameInfo { EventHandlers = "NAMESPACE_Dispatcher", "NAMESPACE_StaticView" }
OptionMenu NAMESPACE_TopMenu { class NAMESPACE_Top Title "Top Points" }
// SPDX-FileCopyrightText: © 2025 Alexander Kromm <mmaulwurff@gmail.com> // SPDX-License-Identifier: BSD-3-Clause // // LazyPoints module version: 0.6.5 // LazyPoints is a part of DoomToolbox: https://github.com/mmaulwurff/doom-toolbox/ // // Point scoring for GZDoom. Do things, get points! // // License: // SPDX-FileCopyrightText: © 2025 Alexander Kromm <mmaulwurff@gmail.com> // SPDX-License-Identifier: BSD-3-Clause // // Scoring: // - Damage: equal to damage amount. // - Telefrag: equal to enemy starting health. // - Kills: 10% of starting health of killed enemy; // - Chain kills: 10 points for each enemy killed in 3 seconds after previous kill. // - Secrets: 250 points. // - Map items: 5 points. // - Keys: 250 points. // - Barrels: 5 points. // - Player with 50% and more health gets kill points (with chain kill bonus) // multiplied by 1.5, with 100% and more - by 2. // // Scoring system is based on ScoreDoom: https://zdoom.org/wiki/ScoreDoom. // // To use (besides ZScript code): // - define cvars: // // nosave string NAMESPACE_score = ""; // user bool NAMESPACE_show = true; // server string NAMESPACE_parameters_class = "NAMESPACE_Parameters"; // // - add event handlers in mapinfo: // // GameInfo { EventHandlers = "NAMESPACE_Dispatcher", "NAMESPACE_StaticView" } // // - add access to top scores menu (add it to a menu or add a key). Menudef: // // OptionMenu NAMESPACE_TopMenu // { // class NAMESPACE_Top // Title "Top Points" // }
1. Source
1.1. Parameters
// LazyPoints customization. class NAMESPACE_Parameters { // This font is used for drawing score points and info on bonuses. virtual Font getFont() const { return Font.getFont("BigFont"); } // This is a time in seconds, during which kills provide a bonus. virtual int getBonusCountdown() const { return 3; } // This is an offset in pixels from the top of the screen for all score info. virtual ui int getYOffset() const { return 10; } // Scale multiplier for all score info. virtual ui int getScale() const { return 1; } // Tells if score points are given for picking up items. virtual bool isPickupBonusEnabled() const { return true; } // Tells if score points are given now. virtual bool isScoringEnabledNow() const { return true; } }
1.2. Dispatcher
// This class is an entry point for LazyPoints. class NAMESPACE_Dispatcher : EventHandler { override void onRegister() { _spawner = new ("NAMESPACE_Spawner").init(); } override void playerEntered(PlayerEvent event) { if (_parameters == NULL) _parameters = NAMESPACE_Parameters(new(NAMESPACE_parameters_class)); if (_mapScore == NULL) _mapScore = new ("NAMESPACE_MapScore").init(); _playerScores[event.playerNumber] = new("NAMESPACE_PlayerScore").init(event.playerNumber, _parameters); } override void renderOverlay(RenderEvent event) { if (menuActive || automapActive) return; _playerScores[consolePlayer].show(event.fracTic); } override void worldThingDamaged(WorldEvent event) { if (event.inflictor == NULL) return; for (int i = 0; i < MAXPLAYERS; ++i) { if (_playerScores[i] != NULL && players[i].mo == event.damageSource) _playerScores[i].countDamage(event.thing, event.damage, event.damageType); } } override void worldThingDied(WorldEvent event) { for (int i = 0; i < MAXPLAYERS; ++i) { if (_playerScores[i] != NULL && players[i].mo == event.thing.target) _playerScores[i].countDeath(event.thing); } } override void worldTick() { for (int i = 0; i < MAXPLAYERS; ++i) { if (_playerScores[i] != NULL) _playerScores[i].tick(); } } override void worldThingSpawned(WorldEvent event) { if (_parameters.isPickupBonusEnabled()) _spawner.spawnScoreFor(event.thing); } override void worldUnloaded(WorldEvent event) { _mapScore.save(); } private NAMESPACE_Spawner _spawner; private NAMESPACE_PlayerScore _playerScores[MAXPLAYERS]; private NAMESPACE_Parameters _parameters; private NAMESPACE_MapScore _mapScore; }
1.3. Bonus View
class NAMESPACE_BonusView { NAMESPACE_BonusView init(NAMESPACE_TimerBonus timerBonus, NAMESPACE_HealthBonus healthBonus, NAMESPACE_Parameters parameters) { _timerBonus = timerBonus; _healthBonus = healthBonus; _parameters = parameters; return self; } ui int show(int y, int scale) { loadFont(); int lineHeight = _font.GetHeight(); y += MARGIN; int bonus = _timerBonus.getBonus(); double multiplier = _healthBonus.getMultiplier(); if (bonus == 0 && multiplier == 1.0) return 0; String bonusString; if (bonus) bonusString.appendFormat("+%d", bonus); if (multiplier != 1.0) { if (bonusString.length()) bonusString.appendFormat(" "); bonusString.appendFormat("x%.1f", multiplier); } int bonusWidth = _font.StringWidth(bonusString) * scale; int x = (Screen.GetWidth() - bonusWidth) / 2; Screen.drawText(_font, Font.CR_Blue, x, y, bonusString, DTA_ScaleX, scale, DTA_ScaleY, scale); return lineHeight * 2 * scale; } const MARGIN = 10; mixin NAMESPACE_FontUser; private NAMESPACE_TimerBonus _timerBonus; private NAMESPACE_HealthBonus _healthBonus; }
1.4. Counter
class NAMESPACE_Counter { NAMESPACE_Counter init(int playerNumber, NAMESPACE_TimerBonus timerBonus, NAMESPACE_HealthBonus healthBonus, NAMESPACE_Parameters parameters) { _player = players[playerNumber]; _oldSecretCount = 0; _timerBonus = timerBonus; _healthBonus = healthBonus; _parameters = parameters; return self; } play void countDamage(Actor damaged, int damage, Name damageType) { if (damageType == "Telefrag") damage = damaged.getSpawnHealth(); if (damaged && damaged.bIsMonster) addPoints(damage); } play void countDeath(Actor died) { addPoints(calculatePointsFor(died)); _timerBonus.registerKill(); } play void countSecrets() { int newSecretCount = _player.SecretCount; if (newSecretCount > _oldSecretCount) { addPoints(250); _oldSecretCount = newSecretCount; } } private play void addPoints(int points) { if (_parameters.isScoringEnabledNow()) _player.mo.score += points; } private play int calculatePointsFor(Actor died) { int result = (died.bIsMonster ? (died.SpawnHealth() / 10 + _timerBonus.getBonus()) : 5) * int(round(_healthBonus.getMultiplier())); return result; } private PlayerInfo _player; private int _oldSecretCount; private NAMESPACE_TimerBonus _timerBonus; private NAMESPACE_HealthBonus _healthBonus; private NAMESPACE_Parameters _parameters; }
1.5. Font User
mixin class NAMESPACE_FontUser { private ui void loadFont() { if (_font == NULL) _font = _parameters.getFont(); } private transient Font _font; private NAMESPACE_Parameters _parameters; }
1.6. Health Bonus
// Health Bonus is a multiplier, which value depends on player health. // // [100%, +inf) - x2 // [ 50%, 100%) - x1.5 // ( 0%, 50%) - x1 // // Credits to ZikShadow for an idea. class NAMESPACE_HealthBonus { NAMESPACE_HealthBonus init(int playerNumber) { _player = players[playerNumber]; return self; } double getMultiplier() { int healthPercent = _player.health * 100 / _player.mo.GetMaxHealth(); if (healthPercent >= 100) return 2.0; else if (healthPercent >= 50) return 1.5; return 1.0; } private PlayerInfo _player; }
1.7. MapScore
class NAMESPACE_MapScore { NAMESPACE_MapScore init() { _startingScore = players[consolePlayer].mo.score; return self; } void save() { int score = players[consolePlayer].mo.score - _startingScore; string checksum = Level.getChecksum(); NAMESPACE_ScoreStorage.saveScore(checksum, score); } private int _startingScore; }
1.8. Map Score Item
class NAMESPACE_MapScoreItem : ScoreItem { NAMESPACE_MapScoreItem init(int n) { amount = n; return self; } Default { -CountItem; +Inventory.Quiet; } }
1.9. Option Menu Score Item
// This class is similar to OptionMenuItemTextField. The difference is that this // class doesn't use a CVar. // // Code is partially borrowed from // gzdoom/wadsrc/static/zscript/ui/menu/optionmenuitems.zs. class OptionMenuScoreItem : OptionMenuItem { OptionMenuScoreItem init(String label, String name, int index, bool isLatest) { Super.init(label, ""); _name = name; _index = index; _enter = NULL; _isLatest = isLatest; return self; } override int draw(OptionMenuDescriptor d, int y, int indent, bool selected) { if (_enter) { // reposition the text so that the cursor is visible when in entering mode. int tLen = Menu.OptionWidth(_name) * CleanXfac_1; int newIndent = screen.GetWidth() - tLen - CursorSpace(); if (newIndent < indent) { indent = newIndent; } } String display = _enter ? (_enter.GetText() .. Menu.OptionFont().GetCursor()) : _name; int unselectedColor = _isLatest ? Font.CR_BLUE : Font.CR_WHITE; int selectedColor = OptionMenuSettings.mFontColorSelection; int color = selected ? selectedColor : unselectedColor; drawLabel(indent, y, color); drawValue(indent, y, color, display); return indent; } override bool, string getString(int i) { if (i == 0) { return true, _name; } return false, ""; } override bool setString(int i, String s) { _name = s; NAMESPACE_ScoreStorage.rename(Level.GetChecksum(), _index, _name); return true; } override bool menuEvent (int mKey, bool fromController) { if (mKey == Menu.MKey_Enter) { bool b; String s; [b, s] = getString(0); Menu.menuSound("menu/choose"); _enter = TextEnterMenu.openTextEnter(Menu.getCurrentMenu(), Menu.optionFont(), s, -1, fromController); _enter.activateMenu(); return true; } else if (mKey == Menu.MKey_Input) { SetString(0, _enter.GetText()); _enter = NULL; return true; } else if (mKey == Menu.MKey_Abort) { _enter = NULL; return true; } return Super.MenuEvent(mkey, fromController); } private String _name; private int _index; private bool _isLatest; private TextEnterMenu _enter; }
1.10. PlayerScore
class NAMESPACE_PlayerScore { NAMESPACE_PlayerScore init(int playerNumber, NAMESPACE_Parameters parameters) { _playerNumber = playerNumber; _parameters = parameters; _timer = new ("NAMESPACE_Timer").init(TICRATE * parameters.getBonusCountdown()); _timerBonus = new ("NAMESPACE_TimerBonus").init(_timer); _healthBonus = new ("NAMESPACE_HealthBonus").init(playerNumber); _counter = new("NAMESPACE_Counter").init(playerNumber, _timerBonus, _healthBonus, parameters); if (playerNumber == consolePlayer) { _view = new ("NAMESPACE_View").init(parameters); _timerView = new ("NAMESPACE_TimerView").init(_timer); _bonusView = new ("NAMESPACE_BonusView").init(_timerBonus, _healthBonus, parameters); } return self; } ui void show(double fracTic) { if (gameState == gs_TitleLevel || _view == NULL) return; if (!isVisible()) return; int y = _parameters.getYOffset(); int scale = _parameters.getScale(); y += _view.show(y, scale); y += _timerView.show(y, scale, fracTic); y += _bonusView.show(y, scale); } play void countDamage(Actor damaged, int damage, Name damageType) { _counter.countDamage(damaged, damage, damageType); } play void countDeath(Actor died) { _counter.countDeath(died); } play void tick() { _counter.countSecrets(); _timer.update(); _timerBonus.update(); } int getPlayerNumber() const { return _playerNumber; } private bool isVisible() { if (_isVisible == NULL) _isVisible = CVar.GetCVar("NAMESPACE_show", players[_playerNumber]); return _isVisible.GetBool(); } private int _playerNumber; private NAMESPACE_Parameters _parameters; private NAMESPACE_Timer _timer; private NAMESPACE_TimerBonus _timerBonus; private NAMESPACE_HealthBonus _healthBonus; private NAMESPACE_Counter _counter; private NAMESPACE_View _view; private NAMESPACE_TimerView _timerView; private NAMESPACE_BonusView _bonusView; transient CVar _isVisible; }
1.11. ScoreStorage
class NAMESPACE_ScoreStorage { static void saveScore(String mapChecksum, int score) { CVar scoreCVar = CVar.FindCVar(STORAGE_CVAR_NAME); String scoreString = scoreCVar.GetString(); let scoreDict = Dictionary.FromString(scoreString); String mapScoresString = scoreDict.At(mapChecksum); Array<int> scores; Array<bool> isLatests; Array<String> names; read(mapScoresString, scores, isLatests, names); for (int i = 0; i < N_TOP; ++i) isLatests[i] = false; for (int i = 0; i < N_TOP; ++i) { if (score > scores[i]) { scores.insert(i, score); isLatests.insert(i, true); names.insert(i, ""); break; } } String newMapScoresString = write(scores, isLatests, names); scoreDict.Insert(mapChecksum, newMapScoresString); String newScoreString = scoreDict.ToString(); scoreCVar.SetString(newScoreString); } static void rename(String mapChecksum, int index, String name) { CVar scoreCVar = CVar.FindCVar(STORAGE_CVAR_NAME); String scoreString = scoreCVar.GetString(); let scoreDict = Dictionary.FromString(scoreString); String mapScoresString = scoreDict.At(mapChecksum); Array<int> scores; Array<bool> isLatests; Array<String> names; read(mapScoresString, scores, isLatests, names); names[index] = name; String newMapScoresString = write(scores, isLatests, names); scoreDict.Insert(mapChecksum, newMapScoresString); String newScoreString = scoreDict.ToString(); scoreCVar.SetString(newScoreString); } static void loadScores(String mapChecksum, out Array<int> scores, out Array<bool> isLatests, out Array<String> names) { CVar scoreCVar = CVar.findCVar(STORAGE_CVAR_NAME); String scoreString = scoreCVar.getString(); let scoreDict = Dictionary.fromString(scoreString); String mapScoresString = scoreDict.at(mapChecksum); read(mapScoresString, scores, isLatests, names); } // Format: // <score>\n<is_latest>\n<name>\n // repeated N_TOP times. private static void read(String scoresString, out Array<int> scores, out Array<bool> isLatests, out Array<String> names) { if (scoresString.Length() == 0) { for (int i = 0; i < N_TOP; ++i) { scores.Push(0); isLatests.Push(0); names.Push(""); } return; } Array<String> tokens; scoresString.Split(tokens, "\n"); int tokenIndex = 0; for (int i = 0; i < N_TOP; ++i) { scores.Push(tokens[tokenIndex++].ToInt()); isLatests.Push(tokens[tokenIndex++].ToInt()); names.Push(tokens[tokenIndex++]); } } private static string write(Array<int> scores, Array<bool> isLatests, Array<String> names) { String result; for (int i = 0; i < N_TOP; ++i) result.appendFormat("%d\n%d\n%s\n", scores[i], isLatests[i], names[i]); return result; } const N_TOP = 10; const STORAGE_CVAR_NAME = "NAMESPACE_score"; }
1.12. Spawner
class NAMESPACE_Spawner { NAMESPACE_Spawner init() { return self; } play void spawnScoreFor(Actor thing) { if (thing && thing.bCountItem) { NAMESPACE_MapScoreItem(Actor.Spawn("NAMESPACE_MapScoreItem", thing.pos)) .init(5); } else if (thing is "Key") { NAMESPACE_MapScoreItem(Actor.Spawn("NAMESPACE_MapScoreItem", thing.pos)) .init(250); } } }
1.13. StaticEventHandler
class NAMESPACE_StaticView : StaticEventHandler { override void onRegister() { _topHintView = new ("NAMESPACE_TopHintView").init(); } override void uiTick() { _topHintView.show(); } private NAMESPACE_TopHintView _topHintView; }
1.14. Timer
// This class counts down ticks. class NAMESPACE_Timer { // Initializes an object with count - number of ticks to count. NAMESPACE_Timer init(int count) { _count = count; _currentCount = 0; return self; } void update() { if (_currentCount) --_currentCount; } void reset() { _currentCount = _count; } int getCount() const { return _currentCount; } int getMaxCount() const { return _count; } private int _count; private int _currentCount; }
1.15. TimerBonus
// Timer bonus is an additive bonus. It simply provides additional points if a // kill is registered in limited time frame (provided by NAMESPACE_Timer). class NAMESPACE_TimerBonus { NAMESPACE_TimerBonus init(NAMESPACE_Timer timer) { _bonus = 0; _timer = timer; return self; } void update() { if (!_timer.getCount()) _bonus = 0; } void registerKill() { _bonus = min(MAX_TIMER_BONUS, _bonus + TIMER_BONUS_STEP); _timer.reset(); } int getBonus() const { return _bonus; } const MAX_TIMER_BONUS = 500; const TIMER_BONUS_STEP = 10; private int _bonus; private NAMESPACE_Timer _timer; }
1.16. TimerView
class NAMESPACE_TimerView { NAMESPACE_TimerView init(NAMESPACE_Timer timer) { _timer = timer; return self; } ui int show(int y, int scale, double fracTic) { int scaledThickness = BAR_THICKNESS * scale; if (_timer.getCount() == 0) return scaledThickness * 2; int screenWidth = Screen.GetWidth(); double ratio = (_timer.getCount() - fracTic) / _timer.GetMaxCount(); int middleWidth = screenWidth / 2; int halfBarWidth = int(round(screenWidth / 8 * ratio)); y += MARGIN; Screen.drawThickLine(middleWidth - halfBarWidth, y, middleWidth + halfBarWidth, y, scaledThickness, BAR_COLOR); return scaledThickness * 2; } const BAR_THICKNESS = 3; const BAR_COLOR = "gray"; const MARGIN = 10; private NAMESPACE_Timer _timer; }
1.17. Top
class NAMESPACE_Top : OptionMenu { override void Init(Menu parent, OptionMenuDescriptor desc) { Super.Init(parent, desc); mDesc.mItems.Clear(); if (gameState != GS_LEVEL && gameState != GS_INTERMISSION) { String label = "No map detected."; addLabel(label); return; } String checksum = Level.GetChecksum(); Array<int> scores; Array<bool> isLatests; Array<String> names; NAMESPACE_ScoreStorage.loadScores(checksum, scores, isLatests, names); int maxLength = 0; for (int i = 0; i < NAMESPACE_ScoreStorage.N_TOP; ++i) { int length = String.Format("%d", scores[i]).Length(); if (length > maxLength) maxLength = length; } // %% will become %. Adds spacing to string output. String format = String.Format("%%d. %%%dd", maxLength); for (int i = 0; i < NAMESPACE_ScoreStorage.N_TOP; ++i) { String label = String.Format(format, i + 1, scores[i]); addCommand(label, names[i], i, isLatests[i]); } } private void addLabel(String label) { mDesc.mItems.Push( new ("OptionMenuItemStaticText").InitDirect(label, Font.CR_WHITE)); } private void addCommand(String label, String name, int index, bool isLatest) { mDesc.mItems.Push( new ("OptionMenuScoreItem").Init(label, name, index, isLatest)); } }
1.18. TopHintView
class NAMESPACE_TopHintView { NAMESPACE_TopHintView init() { _showed = false; return self; } void show() { if (gameState != GS_Intermission) { _showed = false; return; } if (_showed) return; _showed = true; int key1; int key2; [key1, key2] = Bindings.GetKEysForCommand("NAMESPACE_top"); if (key1 == 0 && key2 == 0) return; String keyString = KeyBindings.NameKeys(key1, key2); String hintString = String.Format("\cfPress \ct\"%s\"\cf to show score points.", keyString); Console.Printf(hintString); } private bool _showed; }
1.19. View
class NAMESPACE_View { NAMESPACE_View init(NAMESPACE_Parameters parameters) { _player = players[consolePlayer]; _interpolator = DynamicValueInterpolator.Create(0, 0.1, 1, 1000000); _parameters = parameters; return self; } ui int show(int y, int scale) { loadFont(); int lineHeight = _font.getHeight(); if (!_player.mo) return lineHeight * scale; y += MARGIN; _interpolator.update(_player.mo.score); let scoreString = string.format("%d", _interpolator.getValue()); int scoreWidth = _font.stringWidth(scoreString) * scale; int x = (Screen.GetWidth() - scoreWidth) / 2; Screen.drawText(_font, Font.CR_Blue, x, y, scoreString, DTA_ScaleX, scale, DTA_ScaleY, scale); return lineHeight * scale; } const MARGIN = 10; mixin NAMESPACE_FontUser; private PlayerInfo _player; private DynamicValueInterpolator _interpolator; }
2. Tests
nosave string NAMESPACE_score = ""; user bool NAMESPACE_show = true; server string NAMESPACE_parameters_class = "NAMESPACE_Parameters";
Alias NAMESPACE_top "OpenMenu NAMESPACE_TopMenu" AddKeySection "LazyPoints" "NAMESPACE_keys" AddMenuKey "Open Score" "NAMESPACE_top"
GameInfo { EventHandlers = "NAMESPACE_Dispatcher", "NAMESPACE_StaticView" }
OptionMenu NAMESPACE_TopMenu { class NAMESPACE_Top Title "Top Points" }
version 4.14 #include "LazyPoints.zs"
wait 2; map map01; wait 2; quit