Skip to content

Warcraft III Game Difficulty

Ownership and contract

OpenRealm has three distinct difficulty concerns:

Value Owner Meaning
wc3_campaign_difficulty Warcraft III menu via the client's archived cvar service Profile preference for a new campaign map: 0 Easy, 1 Normal (default), 2 Hard
level.setup.difficulty Warcraft III game module Current map difficulty returned by GetGameDifficulty(); JASS also supports Insane (3)
level.setup.default_difficulty Warcraft III game module Independent default returned by GetDefaultDifficulty(); JASS can change it independently

games/warcraft-3/common/difficulty.h is the common interpretation of the profile string and campaign-path check. The selector and fresh-map initialization use the same bounded parser; malformed, missing, negative, trailing-text, over-range, and Insane profile settings all resolve to Normal. The parser accepts decimal leading zeros but not sign characters or whitespace.

The cvar is registered with CVAR_ARCHIVE by M_Init(), not by loading a map. menuImport_t.Cvar_Register is a generic engine callback: the Warcraft menu names its preference and the universal client delegates registration to Cvar_Get(). Registering an existing variable adds flags without replacing its loaded value. The normal Cvar_WriteConfig / application-exit path persists it in the writable user config, without changing the Warcraft game module's ownership of live simulation difficulty.

Initial map setup

G_SpawnEntities() initializes level.setup.difficulty and level.setup.default_difficulty together before G_InitJassHost() and script initialization. For fresh maps in the Maps\Campaign\ or Maps\FrozenThrone\Campaign\ families (slash or backslash, case-insensitive), the initializer reads the profile preference. Other maps start on Normal, regardless of that preference. JASS may later change current and default values independently, so neither is permanently bound to the profile cvar.

Restart and scripted level transitions

A JASS RestartGame or ChangeLevel (including a direct ChangeLevel without SaveGame) must retain the current and default difficulty separately. G_RequestRestartGame() takes the active level.map_path as its map identity; G_RequestChangeLevel() uses the script's requested map. Both stage a transient wc3DifficultyTransition_t in the game module and queue the existing deferred MenuAction("level") transport. No map load occurs inside the JASS stack.

The game imports the generic ResolveMap capability used by the client menu router (Com_ResolveMapArgument) so a script-supplied basename is staged under its resolved full map path, not under a potentially ambiguous filename. G_LoadMap() passes the actual map filename to G_SpawnEntities(), which calls G_TakeDifficultyTransition() after clearing the old level and before JASS initialization. The one-shot record is consumed regardless of whether the target matches; only an exact full-path match (case/slash insensitive) supplies the recorded pair. Otherwise a fresh map initializes from its campaign preference, or Normal for non-campaign maps. The record is cleared on game initialization/shutdown and normal game exits, and explicitly replaced/cleared on competing game-issued transitions. The universal client never interprets the difficulty values.

SaveGame + ChangeLevel has an additional ordering rule. G_FlushScriptSave() first commits the source snapshot using WriteGame(). Only after success does it stage the transition and send MenuAction("level"). A rejected request or failed source save neither changes maps nor stages a difficulty override. Script LoadGame instead restores the destination save and does not inherit an outgoing map transition. Transient state is never written into the save.

The original do_score_screen argument is still forwarded unchanged through the existing interfaces; implementing score-screen presentation is separate.

For example, Hard / Hard can become Normal / Hard through a script's SetGameDifficulty(Normal). Restarting the same map or invoking ChangeLevel then carries Normal / Hard, leaving the archived Hard preference untouched. Stock Blizzard victory continuations can first restore current from default, so the following chapter enters at Hard / Hard. This patch preserves these values but does not implement the Reduce Difficulty defeat button itself.

Save and load (format 90)

g_save.c now serializes both level.setup.difficulty and level.setup.default_difficulty as separate integer level fields. ReadGame() restores the saved pair rather than reconstructing either value from wc3_campaign_difficulty; loading a temporary Normal / Hard run therefore continues on Normal / Hard even when the menu preference is Easy.

The save format advances from version 89 to 90, rejecting old snapshots as required by Save/load. Transient map-transition records from Patch 2 are not saved; they are only meaningful when starting the next map.

Reduce Difficulty after defeat

Stock CustomDefeatReduceDifficultyBJ changes current difficulty from Hard to Normal or Normal to Easy, leaves default unchanged, unpauses and restarts the mission. Script-authored DialogDisplay actions own their result presentation, including this original Blizzard.j action. OpenRealm's native GameResultDialog fallback now offers a localized Reduce Difficulty action for single-player defeat when the current value is Normal or Hard. It clones the existing GameResultRestartButton FDF template for consistent styling rather than requiring a project-owned FDF. The command validates the defeated player, reduces only the current difficulty and uses G_RequestRestartGame(true) so Patch 2 preserves the current/default pair. It does not change the archived campaign preference. The fallback hides the action for Easy and Insane; the stock script dialog may retain its own Insane visibility semantics.

Completed mission difficulty (Patch 4)

campaign-progress.orcp version 2 stores an independent completion bit and highest successful mission difficulty. On a JASS-authored RemovePlayer(..., PLAYER_GAME_RESULT_VICTORY), the game resolves the full map path against CampaignStrings and records the active level.setup.difficulty if the human player's victory was accepted. The developer win command is excluded. The campaign menu appends the localized best difficulty to completed mission names. Unlocks are unchanged; version-1 availability is migrated without claiming any mission was completed. See Campaign progress.

Verification

menu_fdf.campaign_difficulty_rejects_invalid_preference_and_isolates_melee_maps checks valid/invalid values and both campaign directory families. menu_fdf.campaign_difficulty_registration_marks_loaded_setting_archived checks registration on menu initialization, preservation of a previously loaded value, and emission by Cvar_WriteConfig. The existing campaign selector tests exercise setting the preference and its displayed label. wc3_difficulty_transition.* checks independent current/default values, resolved JASS map basenames, target mismatch one-shot consumption, and failure to resolve a destination. wc3_script_save.successful_save_commits_before_next_level checks that a completed source save carries the independent pair, while wc3_script_save.failed_write_never_starts_next_level asserts that a failed save stages no transition. wc3_save.current_and_default_difficulty_round_trip_independently exercises Normal / Hard and Insane / Easy snapshot restoration. wc3_game regression cases cover server-authoritative native defeat reduction and the fallback button's visibility, styling and command wiring.

See Campaign progress for separate mission unlock persistence, JASS native coverage for runtime difficulty handles, and Save/load for current serialization boundaries.

Campaign AI difficulty coverage (Patch 5)

Difficulty remains script-owned. Retail Scripts/common.ai observes GetGameDifficulty() when campaign AI initializes; the Campaign*Ex helpers choose their Easy/Normal/Hard argument. Unknown higher levels use the Hard quantity for these helpers. SetReplacements likewise selects one of its three authored counts. Nothing in the engine should overwrite those values with a global difficulty multiplier or independently schedule a second set of waves.

wc3_bot.campaign_difficulty_drives_ai_settings_defenders_and_wave_quantities loads a small test AI script in its own JASS VM, once per game difficulty. It checks that this VM reads current mission difficulty, the three-argument quantity selection reaches replacement/defender/assault native state, and Hard-only repair flags are not incorrectly applied at Easy/Normal. The fixture also checks the Hard quantity fallback for Insane. The script is a focused contract fixture, not the complete retail common.ai; it does not prove real campaign wave timing or readiness.

For mission-specific evidence, use the --difficulty-scan mode documented in Campaign map audit. That inventory reports source references and unresolved AI scripts rather than claiming that a branch was executed. Running it with the installed retail MPQs and extending the resulting map-specific tests is required before making broad claims about RoC/TFT difficulty parity.

Known follow-up: player handicap effect

The current SetPlayerHandicap native stores a player percentage in game.clients[..].jass.handicap, and GetPlayerHandicap reads it back. The runtime use of this value in health/max-health calculation is not established by the current source (unlike handicap_xp, which is used in kill XP). The static audit lists maps that call this native so they can be prioritized. Do not treat this as a completed gameplay feature or introduce an arbitrary health multiplier without an authored-unit lifecycle and save/load design.