FinalCustomDoom
Table of Contents
Final Custom Doom provides gameplay customization. It can be used to increase or decrease the difficulty in various ways.
Final Custom Doom is a successor to Ultimate Custom Doom and Custom Doom.
Final Custom Doom is a part of DoomToolbox.
1. License
2. Options
2.1. Player
OptionMenu cd_Player { StaticText "========================================", CDLightBlue StaticText "Player" , CDLightBlue StaticText "========================================", CDLightBlue StaticText "" StaticText "0 disables the option.", CDLightBlue StaticText "" TextField "Weapon damage multiplier", "cd_Player:weaponDamage:Immediately" TextField "Taken damage multiplier" , "cd_Player:takenDamage:Immediately" StaticText "" NumberField "Start health" , "cd_Player:startHealth:OnPlayerStarted" NumberField "Start armor" , "cd_Player:startArmor:OnPlayerStarted" TextField "Save percent" , "cd_Player:savePercent" StaticText "" NumberField "Max health" , "cd_Player:maxHealth:Immediately" TextField "Speed multiplier" , "cd_Player:speedMultiplier:Immediately" TextField "Jump height multiplier" , "cd_Player:jumpMultiplier:Immediately" StaticText "" TextField "Friction multiplier" , "cd_Player:friction:Immediately" TextField "Self damage multiplier" , "cd_Player:selfDamage:Immediately" }
Details
class cd_Player : cd_EffectsBase { static void takenDamage(string value) { pawn().damageFactor = defaultPawn().damageFactor * as0to1Multiplier(value); } static void weaponDamage(string value) { pawn().damageMultiply = defaultPawn().damageMultiply * as0to1Multiplier(value); } static void startHealth(string value) { pawn().a_setHealth(value.toInt()); } static void startArmor(string value) { pawn().giveInventory('cd_StartArmorBonus', value.toInt()); } } class cd_StartArmorBonus : BasicArmorBonus { Default { armor.saveAmount 1; armor.maxSaveAmount 0x7FFFFFFF; } override void beginPlay() { let settings = Dictionary.fromString(cd_settings); double value = settings.at("cd_Player:savePercent").toDouble(); if (value ~== 0) value = 100.0; savePercent = value; } } extend class cd_Player { static void maxHealth(string value) { let pawn = pawn(); int newMaxHealth = value.toInt(); if (newMaxHealth == pawn.maxHealth) return; // 1. Update health items healing ability. let healthFinder = ThinkerIterator.create("Health", Thinker.STAT_DEFAULT); Health healthItem; if (newMaxHealth != 0) { while (healthItem = Health(healthFinder.next())) { // Zero max amount means no limit, leave it so. if (healthItem.maxAmount != 0) continue; healthItem.maxAmount = newMaxHealth * 2; } } else { while (healthItem = Health(healthFinder.next())) healthItem.maxAmount = healthItem.default.maxAmount; } if (newMaxHealth == 0) newMaxHealth = pawn.default.maxHealth; // 2. Set max health and update current health accordingly. int safeMaxHealth = (pawn.maxHealth == 0) ? pawn.default.health : pawn.maxHealth; double relativeHealth = double(pawn.health) / safeMaxHealth; pawn.maxHealth = newMaxHealth; pawn.a_setHealth(int(round(relativeHealth * newMaxHealth))); } static void speedMultiplier(string value) { pawn().speed = defaultPawn().speed * as0to1Multiplier(value); } static void jumpMultiplier(string value) { pawn().jumpZ = defaultPawn().jumpZ * as0to1Multiplier(value); } static void friction(string value) { pawn().friction = defaultPawn().friction * as0to1Multiplier(value); } static void selfDamage(string value) { pawn().selfDamageFactor = defaultPawn().selfDamageFactor * as0to1Multiplier(value); } }
2.2. Actors
OptionMenu cd_Actors { StaticText "========================================", CDLightBlue StaticText "Actors" , CDLightBlue StaticText "========================================", CDLightBlue StaticText "" StaticText "0 disables the option.", CDLightBlue StaticText "" StaticText "Enemies" , White TextField "Health multiplier", "cd_Actors:enemyHealth:OnActorSpawned" NumberField "Health max" , "cd_Actors:enemyHealthMax:OnActorSpawned" TextField "Speed multiplier" , "cd_Actors:enemySpeed:OnActorSpawned" StaticText "" StaticText "Friends" , White TextField "Health multiplier", "cd_Actors:friendHealth:OnActorSpawned" NumberField "Health max" , "cd_Actors:friendHealthMax:OnActorSpawned" TextField "Speed multiplier" , "cd_Actors:friendSpeed:OnActorSpawned" }
Details
class cd_Actors : cd_EffectsBase { static void enemyHealth(string multiplier) { multiplyHealthIf( cd_EventHandler.getLastSpawnedActor(), as0to1Multiplier(multiplier), getSetting("cd_Actors:enemyHealthMax:OnActorSpawned").toInt(), isEnemy); } static void enemyHealthMax(string max) { multiplyHealthIf( cd_EventHandler.getLastSpawnedActor(), as0to1Multiplier(getSetting("cd_Actors:enemyHealth:OnActorSpawned")), max.toInt(), isEnemy); } static void enemySpeed(string multiplier) { multiplySpeedIf( cd_EventHandler.getLastSpawnedActor(), as0to1Multiplier(multiplier), isEnemy); } static void friendHealth(string multiplier) { multiplyHealthIf( cd_EventHandler.getLastSpawnedActor(), as0to1Multiplier(multiplier), getSetting("cd_Actors:friendHealthMax:OnActorSpawned").toInt(), isFriend); } static void friendHealthMax(string max) { multiplyHealthIf( cd_EventHandler.getLastSpawnedActor(), as0to1Multiplier(getSetting("cd_Actors:friendHealth:OnActorSpawned")), max.toInt(), isFriend); } static void friendSpeed(string multiplier) { multiplySpeedIf( cd_EventHandler.getLastSpawnedActor(), as0to1Multiplier(multiplier), isFriend); } private static void multiplyHealthIf(Actor lastSpawned, double multiplier, int max, Function<play bool(Actor)> predicate) { if (lastSpawned == NULL) { Actor anActor; for (let i = ThinkerIterator.create(); anActor = Actor(i.next());) if (predicate.call(anActor)) multiplyHealth(anActor, multiplier, max); } else if (predicate.call(lastSpawned)) multiplyHealth(lastSpawned, multiplier, max); } private static void multiplySpeedIf(Actor lastSpawned, double multiplier, Function<play bool(Actor)> predicate) { if (lastSpawned == NULL) { Actor anActor; for (let i = ThinkerIterator.create(); anActor = Actor(i.next());) if (predicate.call(anActor)) multiplySpeed(anActor, multiplier); } else if (predicate.call(lastSpawned)) multiplySpeed(lastSpawned, multiplier); } private static bool isEnemy(Actor anActor) { return anActor.bIsMonster && !anActor.bFriendly; } private static bool isFriend(Actor anActor) { return anActor.bIsMonster && anActor.bFriendly; } private static void multiplyHealth(Actor anActor, double multiplier, int max) { // For LegenDoom Lite compatibility. let ldlToken = "LDLegendaryMonsterToken"; int ldlMultiplier = (anActor.countInv(ldlToken) > 0) ? 3 : 1; int defaultStartHealth = anActor.default.spawnHealth(); int oldStartHealth = anActor.spawnHealth(); // Some mods have spawn healh as 0??? if (defaultStartHealth == 0) defaultStartHealth = anActor.health; if (oldStartHealth == 0) oldStartHealth = anActor.health; if (defaultStartHealth == 0 || oldStartHealth == 0) return; int oldHealth = anActor.health; let relativeHealth = double(oldHealth) / oldStartHealth; int newStartHealth = int(round(defaultStartHealth * multiplier * ldlMultiplier)); int newHealth = int(round(newStartHealth * relativeHealth)); if (max != 0) { if (newHealth > max) newHealth = max; if (newStartHealth > max) newStartHealth = max; } anActor.startHealth = newStartHealth; anActor.a_setHealth(newHealth); } private static void multiplySpeed(Actor anActor, double multiplier) { anActor.speed = anActor.default.speed * multiplier; } }
2.3. Powerup
OptionMenu cd_Powerups { StaticText "========================================", CDLightBlue StaticText "Permanent powerups" , CDLightBlue StaticText "========================================", CDLightBlue StaticText "" Option "Buddha" , "cd_Powerups:buddha:Periodically" , OnOff Option "Damage" , "cd_Powerups:damage:Periodically" , OnOff Option "Double firing speed", "cd_Powerups:doubleFiringSpeed:Periodically", OnOff Option "Drain" , "cd_Powerups:drain:Periodically" , OnOff Option "Flight" , "cd_Powerups:flight:Periodically" , OnOff Option "Frightener" , "cd_Powerups:frightener:Periodically" , OnOff Option "Ghost" , "cd_Powerups:ghost:Periodically" , OnOff Option "High jump" , "cd_Powerups:highJump:Periodically" , OnOff Option "Infinite ammo" , "cd_Powerups:infiniteAmmo:Periodically" , OnOff Option "Invisibility" , "cd_Powerups:invisibility:Periodically" , OnOff Option "Invulnerability" , "cd_Powerups:invulnerability:Periodically" , OnOff Option "IronFeet" , "cd_Powerups:ironFeet:Periodically" , OnOff Option "LightAmp" , "cd_Powerups:lightAmp:Periodically" , OnOff Option "Mask" , "cd_Powerups:mask:Periodically" , OnOff Option "Minotaur" , "cd_Powerups:minotaur:Periodically" , OnOff Option "Morph" , "cd_Powerups:morph:Periodically" , OnOff Option "Protection" , "cd_Powerups:protection:Periodically" , OnOff Option "Regeneration" , "cd_Powerups:regeneration:Periodically" , OnOff Option "Scanner" , "cd_Powerups:scanner:Periodically" , OnOff Option "Shadow" , "cd_Powerups:shadow:Periodically" , OnOff Option "Speed" , "cd_Powerups:speed:Periodically" , OnOff Option "Strength" , "cd_Powerups:strength:Periodically" , OnOff Option "Targeter" , "cd_Powerups:targeter:Periodically" , OnOff Option "Time freeze" , "cd_Powerups:timeFreeze:Periodically" , OnOff Option "Torch" , "cd_Powerups:torch:Periodically" , OnOff Option "Weapon level 2" , "cd_Powerups:weaponLevel2:Periodically" , OnOff }
Details
class cd_Powerups : cd_EffectsBase { static void buddha (string value) { prolong("PowerBuddha" ); } static void damage (string value) { prolong("PowerDamage" ); } static void doubleFiringSpeed(string value) { prolong("PowerDoubleFiringSpeed"); } static void drain (string value) { prolong("PowerDrain" ); } static void flight (string value) { prolong("PowerFlight" ); } static void frightener (string value) { prolong("PowerFrightener" ); } static void ghost (string value) { prolong("PowerGhost" ); } static void highJump (string value) { prolong("PowerHighJump" ); } static void infiniteAmmo (string value) { prolong("PowerInfiniteAmmo" ); } static void invisibility (string value) { prolong("PowerInvisibility" ); } static void invulnerability (string value) { prolong("PowerInvulnerable" ); } static void ironFeet (string value) { prolong("PowerIronFeet" ); } static void lightAmp (string value) { prolong("PowerLightAmp" ); } static void mask (string value) { prolong("PowerMask" ); } static void minotaur (string value) { prolongMinotaur(); } static void morph (string value) { prolong("PowerMorph" ); } static void protection (string value) { prolong("PowerProtection" ); } static void regeneration (string value) { prolong("PowerRegeneration" ); } static void scanner (string value) { prolong("PowerScanner" ); } static void shadow (string value) { prolong("PowerShadow" ); } static void speed (string value) { prolong("PowerSpeed" ); } static void strength (string value) { prolong("PowerStrength" ); } static void targeter (string value) { prolong("PowerTargeter" ); } static void timeFreezer (string value) { prolong("PowerTimeFreezer" ); } static void torch (string value) { prolong("PowerTorch" ); } static void weaponLevel2 (string value) { prolong("PowerWeaponLevel2" ); } private static void prolong(string power) { let powerup = Powerup(pawn().findInventory(power)); if (powerup == NULL) return; if (powerup.effectTics <= Inventory.BLINKTHRESHOLD + TICRATE) powerup.effectTics += TICRATE; } private static void prolongMinotaur() { prolong("PowerMinotaur"); MinotaurFriend mo; let i = ThinkerIterator.create("MinotaurFriend"); while ((mo = MinotaurFriend(i.next())) != NULL) mo.startTime = level.mapTime; } }
2.4. Health regeneration/degeneration
OptionMenu cd_HealthRegeneration { StaticText "========================================", CDLightBlue StaticText "Health Regeneration" , CDLightBlue StaticText "========================================", CDLightBlue StaticText "" StaticText "0 disables the option.", CDLightBlue StaticText "" NumberField "Amount", "cd_HealthRegeneration:amount:Periodically" Option "Type" , "cd_HealthRegeneration:type", cd_RegenerationType NumberField "Period (seconds)", "cd_HealthRegeneration:period" StaticText "" NumberField "Min", "cd_HealthRegeneration:min" NumberField "Max", "cd_HealthRegeneration:max" StaticText "" Textfield "Sound effect volume" , "cd_HealthRegeneration:sound" TextField "Visual effect intensity", "cd_HealthRegeneration:visual" ColorPicker "Visual effect color" , "cd_HealthRegeneration:color" }
Details
class cd_HealthRegeneration : cd_EffectsBase { static void amount(string amount) { let settings = Dictionary.fromString(cd_settings); if (!isMyTime(settings.at("cd_HealthRegeneration:period").toInt())) return; int type = settings.at("cd_HealthRegeneration:type").toInt(); int min = settings.at("cd_HealthRegeneration:min").toInt(); int max = settings.at("cd_HealthRegeneration:max").toInt(); int old = pawn().health; int target = old + amount.toInt() * (type == Regeneration ? 1 : -1); int new = getNew(old, target, min, max); if (old == new) return; pawn().a_setHealth(new); playSound("cd_health", settings.at("cd_HealthRegeneration:sound").toDouble()); flashColor(settings.at("cd_HealthRegeneration:visual").toDouble(), settings.at("cd_HealthRegeneration:color").toInt()); } }
cd_health = "sounds/540985__magnuswaker__heartbeat-dumpf-dumpf.ogg"
2.5. Armor regeneration/degeneration
OptionMenu cd_ArmorRegeneration { StaticText "========================================", CDLightBlue StaticText "$Armor Regeneration" , CDLightBlue StaticText "========================================", CDLightBlue StaticText "" StaticText "0 disables the option.", CDLightBlue StaticText "" NumberField "Amount", "cd_ArmorRegeneration:amount:Periodically" Option "Type" , "cd_ArmorRegeneration:type", cd_RegenerationType NumberField "Period (seconds)", "cd_ArmorRegeneration:period" StaticText "" NumberField "Min", "cd_ArmorRegeneration:min" NumberField "Max", "cd_ArmorRegeneration:max" StaticText "" TextField "Sound effect volume" , "cd_ArmorRegeneration:sound" TextField "Visual effect intensity", "cd_ArmorRegeneration:visual" ColorPicker "Visual effect color" , "cd_ArmorRegeneration:color" }
Details
class cd_ArmorRegeneration : cd_EffectsBase { static void amount(string amount) { if (pawn().health <= 0) return; let settings = Dictionary.fromString(cd_settings); if (!isMyTime(settings.at("cd_ArmorRegeneration:period").toInt())) return; int type = settings.at("cd_ArmorRegeneration:type").toInt(); int min = settings.at("cd_ArmorRegeneration:min").toInt(); int max = settings.at("cd_ArmorRegeneration:max").toInt(); int old = pawn().countInv('BasicArmor'); int target = old + amount.toInt() * (type == Regeneration ? 1 : -1); int new = getNew(old, target, min, max); if (old == new) return; if (type == Regeneration) pawn().giveInventory('cd_ArmorBonus', new - old); else pawn().takeInventory('BasicArmor', old - new); playSound("cd_armor", settings.at("cd_ArmorRegeneration:sound").toDouble()); flashColor(settings.at("cd_ArmorRegeneration:visual").toDouble(), settings.at("cd_ArmorRegeneration:color").toInt()); } } class cd_ArmorBonus : BasicArmorBonus { Default { armor.saveAmount 1; armor.maxSaveAmount 0x7FFFFFFF; } }
cd_armor = "sounds/778514__blondpanda__denim_and_cloth_step_foley_12.ogg"
2.6. Ammo regeneration
OptionMenu cd_AmmoRegeneration { StaticText "========================================", CDLightBlue StaticText "Ammo Regeneration" , CDLightBlue StaticText "========================================", CDLightBlue StaticText "" StaticText "0 disables the option.", CDLightBlue StaticText "" NumberField "Amount" , "cd_AmmoRegeneration:amount:Periodically" NumberField "Period (seconds)" , "cd_AmmoRegeneration:period" Option "Backpack required", "cd_AmmoRegeneration:backpackRequired", OnOff StaticText "" TextField "Sound effect volume" , "cd_AmmoRegeneration:sound" TextField "Visual effect intensity", "cd_AmmoRegeneration:visual" ColorPicker "Visual effect color" , "cd_AmmoRegeneration:color" }
Details
class cd_AmmoRegeneration : cd_EffectsBase { static void amount(string amountString) { let pawn = pawn(); if (pawn.health <= 0) return; let settings = Dictionary.fromString(cd_settings); if (!isMyTime(settings.at("cd_AmmoRegeneration:period").toInt())) return; bool isBackpackRequired = settings.at("cd_AmmoRegeneration:backpackRequired").toInt(); if (isBackpackRequired && !isBackpackOwned(pawn)) return; int amount = amountString.toInt(); for (int i = 0; i < amount; ++i) { let aBackpack = Inventory(Actor.spawn("Backpack", replace: ALLOW_REPLACE)); aBackpack.clearCounters(); if (!aBackpack.CallTryPickup(pawn)) aBackpack.destroy(); } playSound("cd_ammo", settings.at("cd_ArmorRegeneration:sound").toDouble()); flashColor(settings.at("cd_AmmoRegeneration:visual").toDouble(), settings.at("cd_AmmoRegeneration:color").toInt()); } private static bool isBackpackOwned(PlayerPawn pawn) { return pawn.countInv("Backpack") || pawn.countInv("BagOfHolding") || pawn.countInv("AmmoSatchel"); } }
cd_ammo = "sounds/730748__debsound__bullet-shell-falling-on-concrete-surface-024.ogg"
3. Commands
OptionMenu cd_Commands { StaticText "========================================", CDLightBlue StaticText "Commands" , CDLightBlue StaticText "========================================", CDLightBlue StaticText "" StaticText "Resetting and restoring aren't applied if in a game.", CDLightBlue StaticText "" SafeCommand "$cd_ResetOptions" , cd_reset_to_defaults StaticText "" SafeCommand "$cd_BackupOptions1" , cd_backup_options1 SafeCommand "$cd_RestoreOptions1", cd_restore_options1 StaticText "" SafeCommand "$cd_BackupOptions2" , cd_backup_options2 SafeCommand "$cd_RestoreOptions2", cd_restore_options2 StaticText "" SafeCommand "$cd_BackupOptions3" , cd_backup_options3 SafeCommand "$cd_RestoreOptions3", cd_restore_options3 }
Details
Alias cd_reset_to_defaults "cd_settings \"\"" Alias cd_backup_options1 "cd_settings_profile1 $cd_settings" Alias cd_restore_options1 "cd_settings $cd_settings_profile1" Alias cd_backup_options2 "cd_settings_profile2 $cd_settings" Alias cd_restore_options2 "cd_settings $cd_settings_profile2" Alias cd_backup_options3 "cd_settings_profile3 $cd_settings" Alias cd_restore_options3 "cd_settings $cd_settings_profile3"
server string cd_settings_profile1; server string cd_settings_profile2; server string cd_settings_profile3;
[enu default] cd_ResetOptions = "Reset options to defaults"; cd_BackupOptions1 = "Back up options to Profile 1"; cd_RestoreOptions1 = "Restore options from Profile 1 backup"; cd_BackupOptions2 = "Back up options to Profile 2"; cd_RestoreOptions2 = "Restore options from Profile 2 backup"; cd_BackupOptions3 = "Back up options to Profile 3"; cd_RestoreOptions3 = "Restore options from Profile 3 backup"; [ru] cd_Player = "Игрок";
4. Acknowledgments
- Custom Doom base idea: Lud (Accensus),
- help with developing Custom Doom: JudgeGroovy, Doctrine Dark, Zhs2, Beed28, FaggoStorm, Phantom Allies, FoxBoy, Eruanna.
- help with developing Ultimate Custom Doom: Beed28, przemko27, DabbingSquidward, drogga (Commado Pen), Nems, HexFlareheart, kondoriyano, Spaceman333, ghost.
5. Implementation details
5.1. Menus
AddOptionMenu OptionsMenu { Submenu "$cd_Title", cd_Menu } AddOptionMenu OptionsMenuSimple { Submenu "$cd_Title", cd_Menu } OptionMenu cd_Menu protected { Class cd_Menu cd_PlainTranslator StaticText "========================================", CDLightBlue StaticText "$cd_Title" , CDLightBlue StaticText "========================================", CDLightBlue StaticText "" Submenu "Player" , cd_Player Submenu "Actors" , cd_Actors Submenu "Powerups", cd_Powerups StaticText "" StaticText "Regeneration/Degeneration", White Submenu "Health" , cd_HealthRegeneration Submenu "Armor" , cd_ArmorRegeneration Submenu "Ammo" , cd_AmmoRegeneration StaticText "" Submenu "Commands", cd_Commands } OptionValue cd_RegenerationType { 0, "$cd_Regeneration" 1, "$cd_Degeneration" }
CDLightBlue { #111111 #99CCFF }
// Translation note: most FCD menu items have their strings written in plain English // and not as $, but are still translatable, for example: // TextField "Weapon damage multiplier" "cd_something" // here the string identifier to translate is $cd_Weapon_damage_multiplier. // Normal $ string identifier can be used too. [enu default] cd_Title = "\c[CDLightBlue]⚒\c- Final Custom Doom"; cd_Regeneration = "Regeneration"; cd_Degeneration = "Degeneration"; [ru] cd_Weapon_damage_multiplier = "Множитель урона от оружия";
5.2. Menu item replacements
class cd_Menu : OptionMenu { override void init(Menu parent, OptionMenuDescriptor descriptor) { replaceItems(descriptor.mItems); Super.init(parent, descriptor); } private void replaceItems(out Array<OptionMenuItem> items) { int itemsCount = items.size(); for (int i = 0; i < itemsCount; ++i) items[i] = getReplacement(items[i]); } private OptionMenuItem getReplacement(OptionMenuItem item) { let itemClass = item.getClass(); if (itemClass == 'OptionMenuItemTextField') return new("cd_DoubleField").init(item.mLabel, item.getAction()); if (itemClass == 'OptionMenuItemNumberField') return new("cd_IntField").init(item.mLabel, item.getAction()); if (itemClass == 'OptionMenuItemColorPicker') return new("cd_ColorPicker").init(item.mLabel, item.getAction()); if (itemClass == 'OptionMenuItemOption') { let option = OptionMenuItemOption(item); return new("cd_Option").init(item.mLabel, item.getAction(), option.mValues); } if (itemClass == 'OptionMenuItemSubmenu') { let descriptor = MenuDescriptor.getDescriptor(item.getAction()); replaceItems(OptionMenuDescriptor(descriptor).mItems); return item; } return item; } } mixin class cd_SettingItem { string mTag; private string getSetting() const { return Dictionary.fromString(cd_settings).at(mTag); } private void setSetting(string value) { let settings = Dictionary.fromString(cd_settings); string oldValue = settings.at(mTag); double doubleValue = value.toDouble(); if (doubleValue ~== oldValue.toDouble()) return; if (doubleValue < 0) return; if (doubleValue ~== 0) settings.remove(mTag); else settings.insert(mTag, value); Cvar.getCvar('cd_settings', players[consolePlayer]).setString(settings.toString()); let [_1, _2, _3, when] = cd_EventHandler.parseEffect(mTag); if (when == cd_EventHandler.Immediately || when == cd_EventHandler.OnActorSpawned) EventHandler.sendNetworkEvent(string.format("%s:%s", mTag, value)); } }
server string cd_settings;
class cd_NumberField : OptionMenuItemTextField { mixin cd_SettingItem; string mFormat; OptionMenuItem init(string label, Name command, int decimalPlaces) { mTag = command; mFormat = string.format("%%.%df", decimalPlaces); return Super.init(label, ''); } override bool, string getString(int i) { if (i != 0) return false, ""; return true, string.format(mFormat, getSetting().toDouble()); } override bool setString(int i, string aString) { if (i != 0) return false; setSetting(string.format(mFormat, aString.toDouble())); return true; } override string represent() { return mEnter ? Super.represent() : string.format(mFormat, getSetting().toDouble()); } } class cd_DoubleField : cd_NumberField { OptionMenuItem init(string label, Name command) { return Super.init(label, command, 2); } } class cd_IntField : cd_NumberField { OptionMenuItem init(string label, Name command) { return Super.init(label, command, 0); } } class cd_Option : OptionMenuItemOptionBase { mixin cd_SettingItem; OptionMenuItem init(string label, Name command, Name values) { mTag = command; Super.init(label, '', values, NULL, 0); return self; } override int getSelection() { int valuesCount = OptionValues.getCount(mValues); if (valuesCount <= 0) return -1; if (OptionValues.getTextValue(mValues, 0).length() == 0) { double value = getSetting().toDouble(); for(int i = 0; i < valuesCount; ++i) { if (value ~== OptionValues.getValue(mValues, i)) return i; } } else { string value = getSetting(); for(int i = 0; i < valuesCount; ++i) { if (value ~== OptionValues.getTextValue(mValues, i)) return i; } } return -1; } override void setSelection(int selection) { if (OptionValues.getCount(mValues) <= 0) return; if (OptionValues.getTextValue(mValues, 0).length() == 0) setSetting(string.format("%f", OptionValues.getValue(mValues, selection))); else setSetting(OptionValues.getTextValue(mValues, selection)); } } // Uses a proxy Cvar as a hack just to reuse ColorPickerMenu code. class cd_ColorPicker : OptionMenuItemColorPicker { mixin cd_SettingItem; const CPF_RESET = 0x20001; OptionMenuItem init(string label, Name command) { mTag = command; return Super.init(label, 'cd_proxy_color'); } override int draw(OptionMenuDescriptor desc, int y, int indent, bool selected) { drawLabel(indent, y, selected ? OptionMenuSettings.mFontColorSelection : OptionMenuSettings.mFontColor, isGrayed()); int box_x = indent + cursorSpace(); int box_y = y + CleanYfac_1; Screen.clear(box_x, box_y, box_x + CleanXfac_1 * 32, box_y + CleanYfac_1 * OptionMenuSettings.mLinespacing, getSetting().toInt() | 0xff000000); return indent; } override bool setValue(int i, int v) { if (i != CPF_RESET) return false; setSetting(""); return true; } override bool activate() { Menu.menuSound("menu/choose"); mCvar.setInt(getSetting().toInt()); let desc = OptionMenuDescriptor(MenuDescriptor.getDescriptor('ColorPickerMenu')); let picker = new("cd_ColorPickerMenu"); picker.mTag = mTag; picker.init(Menu.getCurrentMenu(), mLabel, desc, mCvar); picker.activateMenu(); return true; } } // Uses a proxy Cvar as a hack just to reuse ColorPickerMenu code. class cd_ColorPickerMenu : ColorPickerMenu { mixin cd_SettingItem; override void onDestroy() { Super.onDestroy(); setSetting(string.format("%d", Color(int(mRed), int(mGreen), int(mBlue)))); mCvar.setInt(0); } }
user color cd_proxy_color;
5.3. Event handler
GameInfo { EventHandlers = "cd_EventHandler" }
class cd_EventHandler : StaticEventHandler { enum EffecTime { Immediately, OnPlayerStarted, OnActorSpawned, Periodically, Direct, } private clearscope static int toEffectTime(string effectTime) { if (effectTime ~== "Immediately") return Immediately; if (effectTime ~== "OnPlayerStarted") return OnPlayerStarted; if (effectTime ~== "OnActorSpawned") return OnActorSpawned; if (effectTime ~== "Periodically") return Periodically; if (effectTime == "") return Direct; throwAbortException("unknown effect time: %s", effectTime); return Direct; } // Returns class name, function name, value as a string, effect time. // Effect string examples: // cd_ExampleClass:exampleFunction:onPlayerStarted:3.5 // cd_ExampleClass:exampleFunction:3.5 // cd_ExampleClass:exampleFunction:onPlayerStarted static clearscope string, string, string, int parseEffect(string input) { Array<string> parts; input.split(parts, ":"); switch (parts.size()) { case 0: case 1: throwAbortException("no class and function in effect description"); case 2: return parts[0], parts[1], "", Direct; case 3: return parts[0], parts[1], parts[2], toEffectTime(parts[2]); case 4: return parts[0], parts[1], parts[3], toEffectTime(parts[2]); default: throwAbortException("too much parts: %s", input); } return "", "", "", Direct; } private static void callByName(string className, string functionName, string value) { class<Object> aClass = className; if (aClass == NULL) throwAbortException("class %s not found", className); let aFunction = (Function<play void(string)>)(findFunction(aClass, functionName)); if (aFunction == NULL) throwAbortException("function %s.%s not found", className, functionName); aFunction.call(value); } override void networkProcess(ConsoleEvent event) { if (event.name.left(2) ~== "cd") { let [className, functionName, value, when] = parseEffect(event.name); callByName(className, functionName, value); } } private void applyEffects(int effectTime) { let settings = Dictionary.fromString(cd_settings); for (let i = DictionaryIterator.create(settings); i.next();) { let [className, functionName, _, when] = parseEffect(i.key()); if (when == effectTime) callByName(className, functionName, i.value()); } } override void playerEntered(PlayerEvent event) { // TODO: support multiplayer? if (multiplayer) throwAbortException("Final Custom Doom doesn't support multiplayer (yet?)."); PlayerPawn player = players[event.playerNumber].mo; bool isOldGame = (player.findInventory('cd_OldGameMarker') != NULL); if (isOldGame) return; player.giveInventoryType('cd_OldGameMarker'); applyEffects(OnPlayerStarted); applyEffects(Immediately); } private Actor mLastSpawnedActor; static Actor getLastSpawnedActor() { return cd_EventHandler(find('cd_EventHandler')).mLastSpawnedActor; } override void worldThingSpawned(WorldEvent event) { if (event.thing == NULL) return; mLastSpawnedActor = event.thing; applyEffects(OnActorSpawned); mLastSpawnedActor = NULL; } override void worldTick() { if (level.totalTime % TICRATE == 0) applyEffects(Periodically); } } class cd_OldGameMarker : Inventory { Default { inventory.maxAmount 1; +inventory.untossable; } }
5.4. Effects base
class cd_EffectsBase play { enum GenerationType { Regeneration, Degeneration } const BLEND_DURATION = TICRATE / 2; protected static PlayerPawn pawn() { return players[consolePlayer].mo; } protected static readonly<PlayerPawn> defaultPawn() { return getDefaultByType(pawn().getClass()); } // 0 to 1 multipliers: 0.0 acts as 1.0, both meaning it effectively does nothing. protected static double as0to1Multiplier(string stringValue) { double value = stringValue.toDouble(); return (value ~== 0.0) ? 1.0 : value; } protected static bool isMyTime(int period) { return (period != 0) && ((level.totalTime / TICRATE) % period == 0); } protected static void playSound(string sound, double volume) { if (volume != 0.0) pawn().a_startSound(sound, CHAN_AUTO, 0, volume); } protected static void flashColor(double intensity, int aColor) { if (intensity != 0.0) pawn().a_setBlend(aColor, intensity, BLEND_DURATION); } protected static int getNew(int old, int target, int min, int max) { if (min == 0) min = 1; if (max == 0) max = max(old, target); if (!(min <= old && old <= max)) return old; return clamp(target, min, max); } protected static string getSetting(string setting) { return Dictionary.fromString(cd_settings).at(setting); } }
6. Extending Final Custom Doom
You can use Final Custom Doom (FCD) to add your own game settings. To do so, a FCD
extension can be created. Basically, such extension consists of two parts: settings
definition and settings implementation. Settings definition is contained in menudef
lump, where settings are added to cd_Menu, possibly via a submenu. Settings
implementation provides in-game effects and is written in ZScript.
Settings defined in a FCD extension don't have an entry in cvarinfo lump. They are stored, reset to defaults, and backed up to profiles together with FCD settings.
Important note: FCD extensions don't depend on FCD code-wise. This means that they can be loaded without errors even without FCD.
Options in cd_Menu and its submenus don't behave like normal options. The
differences are:
- Several item types are transformed into Custom Doom settings:
TextField-> double setting,NumberField,Option-> int setting,ColorPicker-> color setting.
- Instead of a Cvar, a command is specified in format
"Class:Function:EffectTime":Classis ZScript class name that contains Function. Attention: class name must start withcd.Function: ZScript function name in Class. It must take a string as a parameter, and have return type void (meaning it returns nothing).EffectTime: one of :Immediately,OnPlayerStarted,OnActorSpawned,Periodically, or left out.
- Setting labels in some item types are made directly-translatable. See the note in language.txt in 5.1 section.
See the example below.
// Note: naming everything related to the Custom Doom extension with "cde" prefix. OptionMenu cd_Menu { Submenu "Final Custom Doom Extension", cde_Menu } OptionMenu cde_Menu { Title "Final Custom Doom Extension" StaticText "1. Settings types example:", White TextField "Double setting" , "cde_Effects:doubleSetting:Immediately" NumberField "Integer setting", "cde_Effects:intSetting:Immediately" Option "Option" , "cde_Effects:optionSetting:Immediately", cde_Values ColorPicker "Color setting" , "cde_Effects:colorSetting:Immediately" StaticText "" StaticText "2. Settings apply times example:", White TextField "Applied immediately" , "cde_Effects:setting1:Immediately" TextField "Applied on player start", "cde_Effects:setting2:OnPlayerStarted" TextField "Applied on actor spawn" , "cde_Effects:setting3:OnActorSpawned" TextField "Applied every second" , "cde_Effects:setting4:Periodically" // A setting that isn't applied by itself is used from other settings, // see how to get its value in setting1 function. TextField "Isn't applied" , "cde_Effects:setting5" } OptionValue cde_Values { 0, "Value 1" 1, "Value 2" }
version 4.14.2
class cde_Effects
{
static void doubleSetting(string value)
{
Console.printf("Double setting is set to %f.", value.toDouble());
}
static void intSetting(string value)
{
Console.printf("Integer setting is set to %d.", value.toInt());
}
static void optionSetting(string value)
{
Console.printf("Option setting is set to %d.", value.toInt());
}
static void colorSetting(string value)
{
Console.printf("Color setting is set to %x.", value.toInt());
}
static void setting1(string value)
{
let settingsCvar = Cvar.getCvar("cd_settings");
let settings = Dictionary.fromString(settingsCvar.getString());
let setting = settings.at("cde_Effects:setting5").toDouble();
Console.printf("Setting 1 is applied immediately. Setting 5 is %f.", setting);
}
static void setting2(string value)
{
Console.printf("Setting 2 is applied on player start.");
}
static void setting3(string value)
{
Console.printf("Setting 3 is applied on actor spawned.");
}
static void setting4(string value)
{
Console.printf("Setting 4 is applied periodically.");
}
// Setting 5 isn't applied by itself and doesn't need a function.
}