10.5x
Table of Contents
2019 Cacowards Mini Mod Safari:
Make anything into a slaughtermap, or make nuts.wad not melt your CPU, all from the reach of a single slider.
Required ZScript version:
4.14
Features:
- Can increase or decrease the number of enemies;
- Should work with enemy replacers and randomizers;
Options:
- Global enemy multiplier.
- Enemy multiplier by type (set the global multiplier to 0 and start the map).
- Resurrection of automatically killed enemies (
x5_raise_dividedCvar, off by default).
Anti-features:
- No display of remaining enemies;
- May not work well with scripted maps;
- Doesn't affect enemies spawned after the map is loaded;
- Suggested max enemy amount: no, because it would limit enemy numbers disproportionately to the map size.
Known issues (for multiplier > 1):
- Tags 666 and 667 may be triggered more than once per map;
- Close-range enemies (like demons) may stand too close to the player to be hit.
- It may not be possible to get 100% kills in maps with single-use monster teleporters.
Acknowledgments:
- Original idea: Cutmanmike;
- Code used as a reference: The Zombie Killer;
- Bug reporting: Jimmy, Nems, murka, Arkezuli, Nightsentinel, Rowsol, StroggVorbis;
- Feature suggestions: Rowsol, Accensus;
- Translation support and Russian translation: Blueberryy;
- Brazilian Portuguese localization: generic name guy.
10.5x is a part of DoomToolbox.
1. Licenses
2. x5_EventHandler
GameInfo { EventHandlers = "x5_EventHandler" }
server int x5_multiplier = 100;
class x5_EventHandler : EventHandler { // 1. Entry point. override void worldLoaded(WorldEvent event) { if (level.mapName ~== "titlemap") { destroy(); return; } collectSpawnPoints(mSpawnPoints); if (mSpawnPoints.size() == 0) { destroy(); return; } mEnemyTypes = collectEnemyTypes(mSpawnPoints); if (x5_multiplier == 0) { // Each enemy type has its own multiplier, ask to fill multipliers. mTypeMultipliers = NULL; mFreezer = x5_Freezer.init(); mFreezer.freeze(); if (consolePlayer == net_arbitrator) sendInterfaceEvent(consolePlayer, "x5_menu"); else Console.midPrint(null, StringTable.Localize("$X5_HOST_SELECTS"), true); } else { mTypeMultipliers = fillTypeMultipliers(mEnemyTypes, x5_multiplier); } } override void interfaceProcess(ConsoleEvent event) { if (event.name != "x5_menu") return; Menu.setMenu("x5_TypeMultipliers"); x5_TypeMultipliersMenu(Menu.getCurrentMenu()).setUp(self, mEnemyTypes); } override void worldTick() { // wait for type multipliers. if (mTypeMultipliers == NULL) return; if (mFreezer != NULL) mFreezer.thaw(); if (level.mapTime > TIME_TO_RANDOMIZE) { multiply(); mTypeMultipliers = NULL; } } override void networkProcess(ConsoleEvent event) { if (event.name.left(3) != "x5_") return; mTypeMultipliers = Dictionary.fromString(event.name.mid(3)); } override void worldThingSpawned(WorldEvent event) { Actor thing = event.thing; if (thing == NULL || !isCloneable(thing)) return; if (thing.bThruActors || thing.checkMove(thing.pos.xy)) return; // thing.a_SetRenderStyle(1, STYLE_Stencil); // for debugging. thing.bThruActors = true; thing.a_GiveInventory('x5_WalkAbilityWatcher', 1); } private static void collectSpawnPoints(out Array<x5_SpawnPoint> result) { Actor anActor; for (let i = ThinkerIterator.create("Actor"); anActor = Actor(i.next());) { let replaceeType = Actor.getReplacee(anActor.getClassName()); if (!isCloneable(getDefaultByType(replaceeType))) continue; let spawnPoint = new ("x5_SpawnPoint"); spawnPoint.position = anActor.pos; spawnPoint.height = anActor.height; spawnPoint.radius = anActor.radius; spawnPoint.replaceeType = replaceeType; spawnPoint.original = anActor; result.push(spawnPoint); } } private static Dictionary collectEnemyTypes(Array<x5_SpawnPoint> spawnPoints) { let result = Dictionary.create(); foreach (spawnPoint : spawnPoints) { result.insert(spawnPoint.replaceeType.getClassName(), "100"); } return result; } private static Dictionary fillTypeMultipliers(Dictionary enemyTypes, int multiplier) { let result = Dictionary.create(); let formattedMultiplier = string.format("%d", multiplier); for (let i = DictionaryIterator.create(enemyTypes); i.next();) result.insert(i.key(), formattedMultiplier); return result; } private void multiply() { for (let i = DictionaryIterator.create(mTypeMultipliers); i.next();) { int multiplier = i.value().toInt(); if (multiplier == 100) continue; class<Actor> type = i.key(); Array<Actor> enemiesByType; collectSpawnedEnemiesByType(type, enemiesByType); multiplyEnemies(type, enemiesByType, multiplier); } } private void collectSpawnedEnemiesByType(class<Actor> type, out Array<Actor> enemiesByType) { foreach (spawnPoint : mSpawnPoints) { if (spawnPoint.replaceeType != type) continue; // If the actor is still present, great! // Otherwise, assume the spawned actor isn't far away. if (spawnPoint.original != NULL) { enemiesByType.push(spawnPoint.original); } else { let pos = spawnPoint.position; let height = spawnPoint.height; let radius = spawnPoint.radius; let i = BlockThingsIterator.createFromPos(pos.x, pos.y, pos.z, height, radius, false); while (i.next()) { if (((pos.x, pos.y) - i.thing.pos.xy).length() < radius) { enemiesByType.push(i.thing); break; } } } } } private static void multiplyEnemies(class<Actor> originalType, Array<Actor> enemies, int multiplier) { if (multiplier == 100) return; // console.printf("multiply time: %d, multiplier: %d", level.time, multiplier); int integerMultiplier = multiplier / 100; int copiesNumber = integerMultiplier - 1; foreach (enemy : enemies) { if (multiplier == 0) { let killer = x5_Killer(Actor.spawn("x5_Killer", x5_Killer.makePosition(enemy))); killer.init(enemy); } else { for (int c = 0; c < copiesNumber; ++c) clone(originalType, enemy); } } if (multiplier % 100 == 0) return; shuffle(enemies); double fractionMultiplier = (multiplier % 100) * 0.01; uint enemiesNumber = enemies.size(); uint stp = uint(round(enemiesNumber * fractionMultiplier)); if (integerMultiplier >= 1) // add { for (uint i = 0; i < stp; ++i) clone(originalType, enemies[i]); } else // decimate { for (uint i = stp; i < enemiesNumber; ++i) { let killer = x5_Killer(Actor.spawn("x5_Killer", x5_Killer.makePosition(enemies[i]))); killer.init(enemies[i]); } } } private static void clone(class<Actor> originalType, Actor enemy) { let spawned = Actor.spawn(originalType, enemy.pos, ALLOW_REPLACE); spawned.bAmbush = enemy.bAmbush; // copied from randomspawner.zs spawned.spawnAngle = enemy.spawnAngle; spawned.angle = enemy.angle; spawned.pitch = enemy.pitch; spawned.roll = enemy.roll; spawned.spawnPoint = enemy.spawnPoint; spawned.special = enemy.special; spawned.args[0] = enemy.args[0]; spawned.args[1] = enemy.args[1]; spawned.args[2] = enemy.args[2]; spawned.args[3] = enemy.args[3]; spawned.args[4] = enemy.args[4]; spawned.special1 = enemy.special1; spawned.special2 = enemy.special2; // MTF_SECRET needs special treatment to avoid incrementing the secret // counter twice. It had already been processed for the spawner itself. spawned.spawnFlags = enemy.spawnFlags & ~MTF_SECRET; spawned.handleSpawnFlags(); spawned.spawnFlags = enemy.spawnFlags; // "Transfer" count secret flag to spawned actor spawned.bCountSecret = enemy.spawnFlags & MTF_SECRET; spawned.changeTid(enemy.tid); spawned.vel = enemy.vel; // For things such as DamageMaster/DamageChildren, transfer mastery. spawned.master = enemy.master; spawned.target = enemy.target; spawned.tracer = enemy.tracer; spawned.copyFriendliness(enemy, false); } // TODO: don't clone non-killable enemies? Find examples (Eviternity II turrets)? private static bool isCloneable(readonly<Actor> anActor) { return anActor.bIsMonster && !anActor.bFriendly && anActor.bCountKill; } private static void shuffle(out Array<Actor> actors) { // Fisher-Yates shuffle. uint numberOfActors = actors.size(); for (uint i = numberOfActors - 1; i >= 1; --i) { int j = random[x105](0, i); let temp = actors[i]; actors[i] = actors[j]; actors[j] = temp; } } // There are mods that have randomization that takes a few tics. const TIME_TO_RANDOMIZE = 4; private Dictionary mEnemyTypes; private Dictionary mTypeMultipliers; private Array<x5_SpawnPoint> mSpawnPoints; private x5_Freezer mFreezer; } // class x5_EventHandler
class x5_SpawnPoint { vector3 position; double height; double radius; class<Actor> replaceeType; Actor original; }
3. x5_Freezer
class x5_Freezer play { static x5_Freezer init() { let result = new ("x5_Freezer"); result.mWasFrozen = false; result.mWasLevelFrozen = false; return result; } void freeze() { if (mWasFrozen) return; mWasFrozen = true; freezeLevel(); freezePlayer(); } void thaw() { if (!mWasFrozen) return; mWasFrozen = false; thawLevel(); thawPlayer(); } private void freezeLevel() { mWasLevelFrozen = level.isFrozen(); level.setFrozen(true); } private void freezePlayer() { mWasPlayerFrozen = true; PlayerInfo player = players[consolePlayer]; mCheats = player.cheats; mVelocity = player.mo.vel; mGravity = player.mo.gravity; setPlayerFrozen(player.cheats | FROZEN_CHEATS_FLAGS, (0, 0, 0), 0); } private void thawLevel() const { level.setFrozen(mWasLevelFrozen); } private void thawPlayer() const { if (mWasPlayerFrozen) setPlayerFrozen(mCheats, mVelocity, mGravity); mWasPlayerFrozen = false; } private static void setPlayerFrozen(int cheats, vector3 velocity, double gravity) { PlayerInfo player = players[consolePlayer]; if (player.mo == NULL) return; player.cheats = cheats; player.vel = velocity.xy; player.mo.vel = velocity; player.mo.gravity = gravity; } const FROZEN_CHEATS_FLAGS = CF_TotallyFrozen | CF_Frozen; private bool mWasFrozen; private bool mWasLevelFrozen; private bool mWasPlayerFrozen; private int mCheats; private vector3 mVelocity; // to reset weapon bobbing. private double mGravity; } // class x5_Freezer
4. x5_WalkAbilityWatcher
This inventory item resets bThruActors flag if the actor is able to move without it.
class x5_WalkAbilityWatcher : Inventory { override void tick() { owner.bThruActors = false; bool ownerCanMove = owner.checkMove(owner.pos.xy); if (ownerCanMove) { //owner.a_SetRenderStyle(1, STYLE_Normal); // for debugging. owner.removeInventory(self); destroy(); return; } else { owner.bThruActors = true; } Super.tick(); } } // class x5_WalkAbilityWatcher
5. x5_Killer
This class kills an enemy when the enemy becomes active. Such an enemy is marked with
a floating icon. Whether an enemy killed by x5_Killer can be resurrected is
controlled by x5_raise_divided Cvar.
server bool x5_raise_divided = false;
class x5_Killer : Actor { Default { Height 30; FloatBobStrength 0.2; RenderStyle 'translucent'; // Change this to 'none' to hide killer marks. Alpha 0.3; +NoBlockmap; +NoGravity; +DontSplash; +NotOnAutomap; +FloatBob; +Bright; } States { Spawn: m8rd A - 1; Stop; } override void tick() { Super.tick(); if (mWatched == NULL) { destroy(); return; } setOrigin(makePosition(mWatched), true); if (mWatched.health > 0 && mWatched.target == NULL) return; mWatched.a_Die(); mWatched.bCorpse = x5_raise_divided; destroy(); } void init(Actor watched) { mWatched = watched; } static vector3 makePosition(Actor watched) { return watched.pos + (0, 0, watched.height * 1.5); } private Actor mWatched; } // class x5_Killer
6. language
// SPDX-FileCopyrightText: 2020 Blueberryy [enu default] X5_TYPE_MENU_TITLE = "10.5x Enemy Multipliers"; X5_EXIT = "Exit this menu to start the level."; X5_000 = "Per enemy type (on level start)"; X5_HOST_SELECTS = "The host selects the multipliers"; [ru] X5_TYPE_MENU_TITLE = "10.5x Коэффициенты врагов"; X5_EXIT = "Выйдите из этого меню, чтобы начать уровень."; X5_000 = "По типу врагов (при старте уровня)"; X5_HOST_SELECTS = "Сервер выбирает коэффициенты";
7. Global multiplier
7.1. OptionMenuItemX5Slider
AddOptionMenu OptionsMenu
{
X5Slider "", x5_multiplier, 0, 10.5, 0.1, 1
}
class OptionMenuItemX5Slider : OptionMenuItemSlider { OptionMenuItemX5Slider init(string label, name command, double min, double max, double step, int showval = 1) { Super.init(label, command, min, max, step, showval); setLabel(mCvar.getInt()); return self; } override double getSliderValue() { return (mCvar.getInt() / 100.0); } override void setSliderValue(double val) { int v = int(round(val * 100)); mCvar.setInt(v); setLabel(v); } private void setLabel(int val) { mLabel = (val == 0) ? StringTable.localize("$X5_000").." 10.5x:" : "10.5x:"; } } // class OptionMenuItemX5Slider
8. Type multipliers
8.1. x5_TypeMultipliersMenu
OptionMenu "x5_TypeMultipliers"
{
Class "x5_TypeMultipliersMenu"
Title "$X5_TYPE_MENU_TITLE"
}
nosave string x5_type_multipliers = "";
class x5_TypeMultipliersMenu : OptionMenu { override bool menuEvent(int mKey, bool fromController) { if (mKey == MKey_Back) report(); return Super.menuEvent(mKey, fromController); } void setUp(EventHandler anEventHandler, Dictionary enemyTypes) { mEventHandler = anEventHandler; mDesc.mItems.clear(); mDesc.mSelectedItem = 2; string description = StringTable.localize("$X5_EXIT"); mDesc.mItems.push( new ("OptionMenuItemStaticText").initDirect(description, Font.CR_Black)); mDesc.mItems.push(new ("OptionMenuItemStaticText").init("")); let savedMultipliers = Dictionary.fromString(x5_type_multipliers); for (let i = DictionaryIterator.create(savedMultipliers); i.next();) { string type = i.key(); if (enemyTypes.at(type).length() != 0) { int multiplier = i.value().toInt(); enemyTypes.insert(type, string.format("%d", multiplier)); } } Array<x5_TypeSortElement> types; for (let i = DictionaryIterator.create(enemyTypes); i.next();) { class<Actor> enemyClass = i.key(); int multiplier = i.value().toInt(); let defaultEnemy = getDefaultByType(enemyClass); let element = new ("x5_TypeSortElement"); element.mName = defaultEnemy.getTag(); element.mHealth = defaultEnemy.health; element.mClass = enemyClass; element.mMultiplier = multiplier; types.push(element); } sortTypes(types); foreach (element : types) { let slider = new ("OptionMenuItemX5TypeSlider"); slider.init(element.mClass, element.mMultiplier); mDesc.mItems.push(slider); } } private void report() { let savedMultipliers = Dictionary.fromString(x5_type_multipliers); let multipliersToReport = Dictionary.create(); foreach (menuItem : mDesc.mItems) { let slider = OptionMenuItemX5TypeSlider(menuItem); if (slider == NULL) continue; string className = slider.getEnemyClassName(); string multiplier = string.format("%d", slider.getValue()); multipliersToReport.insert(className, multiplier); savedMultipliers.insert(className, multiplier); } Cvar.findCvar("x5_type_multipliers").setString(savedMultipliers.toString()); string event = string.format("x5_%s", multipliersToReport.toString()); mEventHandler.sendNetworkEvent(event); } private void sortTypes(out Array<x5_TypeSortElement> types) { // Gnome sort (stupid sort): https://en.wikipedia.org/wiki/Gnome_sort let pos = 0; let length = types.size(); while (pos < length) { if (pos == 0 || isGreaterOrEqual(types[pos], types[pos - 1])) { ++pos; } else { // swap let tmp = types[pos]; types[pos] = types[pos - 1]; types[pos - 1] = tmp; --pos; } } } private bool isGreaterOrEqual(x5_TypeSortElement lhs, x5_TypeSortElement rhs) { if (lhs.mHealth > rhs.mHealth) return true; if (lhs.mHealth == rhs.mHealth && lhs.mName >= rhs.mName) return true; return false; } private EventHandler mEventHandler; } // class x5_TypeMultipliersMenu
class x5_TypeSortElement { string mName; int mHealth; class<Actor> mClass; int mMultiplier; }
8.2. OptionMenuItemX5TypeSlider
class OptionMenuItemX5TypeSlider : OptionMenuItemSlider { void init(class<Actor> enemyClass, int value) { Super.init(getDefaultByType(enemyClass).getTag(), "", 0, 10.5, 0.1, 1); mValue = value; mEnemyClassName = enemyClass.getClassName(); } override double getSliderValue() { return (mValue / 100.0); } override void setSliderValue(double value) { mValue = int(round(value * 100)); } string getEnemyClassName() { return mEnemyClassName; } int getValue() { return mValue; } private int mValue; private string mEnemyClassName; }
9. Sprites
sprites/m8rda0.png:
10. Tests
GameInfo { EventHandlers = "x5t_Test", "x5t_Quoter" }
server string x5t_name = ""; server string x5t_spawn = "";
class x5t_Clematis : Clematis {}
class x5t_Test : StaticEventHandler { override void onRegister() { setOrder(-1); } override void networkProcess(ConsoleEvent event) { if (event.name == "x5t_begin") { mTest = new ("x5t_Clematis"); mTest.describe("10.5x test"); } else if (event.name.left(10) == "x5t_expect") { let expected = Dictionary.fromString(x5t_Quoter.quote(event.name.mid(10))); for (let i = DictionaryIterator.create(expected); i.next();) testActorClass(x5t_name, i.value().toInt(), i.key()); } else if (event.name == "x5t_end") { mTest.endDescribe(); } } override void worldLoaded(WorldEvent event) { int width = getDefaultByType('DoomImp').radius * 2; int yBegin = -2 * width; int yEnd = 2 * width; int x = 100; int y = yBegin; // console.printf("spawn time: %d, x: %d, spawn: %s", // level.time, x5_multiplier, x5t_spawn); let spawn = Dictionary.fromString(x5t_Quoter.quote(x5t_spawn)); for (let i = DictionaryIterator.create(spawn); i.next();) { int count = i.value().toInt(); for (int c = 0; c < count; ++c) { Actor.spawn(i.key(), players[consolePlayer].mo.pos + (x, y, 0), ALLOW_REPLACE); y += width; if (y > yEnd) { y = yBegin; x += width; } } } } private void testActorClass(string testName, int expectedCount, string actorClassName) { int aliveCount = 0; int canMoveCount = 0; let i = ThinkerIterator.create(actorClassName); for (Actor anActor = Actor(i.next()); anActor != NULL; anActor = Actor(i.next())) { aliveCount += (anActor.health > 0); canMoveCount += (anActor.health > 0) && anActor.checkMove(anActor.pos.xy); } string description = testName..": "..actorClassName; mTest.it(description..": alive", mTest.assertEval(aliveCount, "==", expectedCount)); mTest.it(description..": can move", mTest.assertEval(canMoveCount, "==", expectedCount)); } private Clematis mTest; } // class x5t_Test
class x5t_Quoter : EventHandler { static string quote(string input) { input.replace("'", "\""); return input; } override void NetworkProcess(ConsoleEvent event) { if (event.name.left(3) == "x5r") sendNetworkEvent("x5_"..quote(event.name.mid(3))); } }
Doom monsters with radius 20:
| Monster | In tests | Replacement |
|---|---|---|
Archvile |
Yes | x5t_Archvile via RandomSpawner |
DoomImp |
Yes | No |
Revenant |
Yes | x5t_Revenant via A_SpawnItemEx |
ZombieMan |
Yes | No |
ShotgunGuy |
||
ChaingunGuy |
||
WolfensteinSS |
// clang-format off class x5t_Archvile : Archvile {} class x5t_Revenant : Revenant {} // clang-format on class x5t_ArchvileReplacer : RandomSpawner replaces Archvile { Default { DropItem "x5t_Archvile"; } } /// Based on switch-based replacements from Brutal Doom v21. class x5t_RevenantReplacer : Actor replaces Revenant { States { Spawn: TNT1 A 0 { bThruActors = 1; bCountKill = 0; } TNT1 A 0 a_SpawnItemEx("x5t_Revenant", 0, 0, 0, 0, 0, 0, 0, SXF_NoCheckPosition | SXF_TransferAmbushFlag, 0); Stop; } }
TODO: make multiplayer tests, maybe?
x5_multiplier 100
wait 2; map map01
wait 4; netevent x5t_begin
wait 6; x5t_name IntegerMultiplier; x5_multiplier 300
wait 8; x5t_spawn {'DoomImp':'1','ZombieMan':'1'}
wait 10; map map01
wait 19; netevent x5t_expect{'DoomImp':'3','ZombieMan':'3'}
wait 28; x5t_name FractionalMultiplier; x5_multiplier 270
wait 30; x5t_spawn {'DoomImp':'10'}
wait 32; map map01
wait 41; netevent x5t_expect{'DoomImp':'27'}
wait 50; x5t_name Divider; x5_multiplier 70
wait 52; x5t_spawn {'DoomImp':'10'}
wait 54; map map01
wait 63; turn180
wait 72; +attack
wait 81; -attack
wait 90; netevent x5t_expect{'DoomImp':'7'}
wait 99; x5t_name PerClass; x5_multiplier 0
wait 101; x5t_spawn {'DoomImp':'1','ZombieMan':'1'}
wait 103; map map01
wait 112; netevent x5r{'DoomImp':'300','ZombieMan':'500'}; closemenu
wait 121; netevent x5t_expect{'DoomImp':'3','ZombieMan':'5'}
wait 130; x5t_name RandomSpawner; x5_multiplier 200
wait 132; x5t_spawn {'Archvile':'1'}
wait 134; map map01
wait 143; netevent x5t_expect{'x5t_Archvile':'2'}
wait 152; x5t_name A_SpawnItemEx; x5_multiplier 200
wait 154; x5t_spawn {'Revenant':'1'}
wait 156; map map01
wait 165; netevent x5t_expect{'x5t_Revenant':'2'}
wait 174; netevent x5t_end
wait 176; quit