Witcher Wiki
Substances Graveir bone
Expansion required
This article is too short to provide more than rudimentary information about the subject. You can help Witcher Wiki by expanding it.

Modifying scripts in the Witcher 3 is the primary way to modify game behavior, though you should know the basics behind programming before you try to jump in. Script mods, unlike other mods, do not need to be cooked, packaged, or bundled, and sit as loose files (in the same directory structure as the vanilla scripts) in your mod folder's /content/scripts/ directory. When the game is started, all mod scripts are loaded and any scripts matching a vanilla script (i.e. same file name in the same directory structure) replace the vanilla script and are loaded instead. You should be aware of the limitations as well as the workarounds that exist for script modding.


Tips and Best Practices[]

Do No Harm (and mind your whitespace edits!)[]

While modding, you should keep in mind that every change that you make can affect other mods that a user installs, so you should do your best to keep your changes self-contained and with as few side-effects as you can. You should also try to avoid changing things like whitespace in sections of code that you are not modifying, because these changes can make it difficult for a user to merge in your mod.

Comment Tags[]

You should clearly mark sections of code that are modified or added by your mod. If you are changing a single line, you could use a simple comment tacked on to the end of the line like this:

default useNativeTargeting = false; //modEnhancedTargeting

or if you're modifying a larger section of code, you should have a begin and end comment, something like this:

//modEnhancedTargeting BEGIN
MyModdedCode();
MyModdedCode2();
MyModdedCode3();
MyModdedCode4();
//modEnhancedTargeting END

Adding comment tags like this make it obvious which sections of code that you've modified so that users installing your mod can easily merge your mod in.

Use Script Studio (even if you don't use it to edit script)[]

Script Studio can be an incredible tool if you know how to use it and its limitations. Don't forget that you can use it to trigger a script recompile without launching the game.

Use the scriptslog[]

Instead of using the combat log or sounds to help debug your program, consider using the logging functions built into the game. An article on how to enable the scriptslog and how to use it is available here. This has the positive side effect of not being needed to comment your debug code out before you package your mod and put it up for download, and giving another way of having users diagnose problems with you—just have them turn on the scriptslog and send you the log!

Use Commented 1.21 Scripts[]

In version 1.21 scripts were extensively commented, which can be really helpful when working with them.

You can download them from nexus here http://www.nexusmods.com/witcher3/mods/1685/?

Game Facts System (Saving/Loading Data)[]

Facts are used to store data in saves. (when the game is saved) They can be used to save and load type int directly.

Saving[]

function FactsAdd( ID : string, optional value : int, optional validFor : int  )

Fact by ID passed as first argument is created (if it didn't exist), adds value (also to existing fact) passed as second argument, valid for value of type int passed as third argument(but I'm not sure how validFor works, probably it's pointing to some predefined game time value) if you want your fact to be valid forever, as a third argument pass - 1 or just don't pass third argument.

function FactsSet(ID : string, val : int, optional validFor : int )

Sets fact by ID passed as first argument to value passed as second argument, optionally sets its validFor to value passed as third argument. Fact is automatically created if it didn't exist before.

function FactsSubstract(ID : string, optional val : int)

From a fact by ID passed as first argument, substracts value passed as second argument, val : int is optional, if you only pass fact ID to the function, it will substract 1 from the fact total value.

Loading[]

function FactsQuerySum( ID : string ) : int

Returns value of fact by ID passed to it as argument.

function FactsDoesExist( ID : string ) : bool

Returns true if fact by ID passed as an argument exists, false if it doesn't.

Other[]

function FactsRemove( ID : string ) : bool

Removes fact by ID passed as argument.

Functions not tested[]

function FactsQuerySumSince( ID : string, sinceTime : EngineTime ) : int
function FactsQueryLatestValue( ID : string ) : int

See scripts/game/facts.ws, there are also "GameplayFacts", only difference seems to be that they can be set to be valid for exact time value instead of predefined values like "normal" facts, but you will have to test it.

Other Method[]

There is another method for saving data with scripts, not related to game saves, using user.settings. See Menus in The Witcher 3/Scripting

Executing Code On Game Load And Location Change[]

When you want your code to be executed on Game Load or Location Change, you can use CR4Game event OnAfterLoadingScreenGameStart(). (scripts\game\r4Game.ws)

Or W3PlayerWitcher event OnSpawned(). (scripts\game\player\playerWitcher.ws)

Simply call your function or anything you want there.

Executing Code On Closing Main Menu[]

When you want to execute code on closing menu, for example to update scripts with new values of variables modified in menu, you can use CR4CommonIngameMenu event OnClosingMenu() (scripts\game\gui\main_menu\commonIngameMenu.ws)

Simply call your function at the end of it. Now it will be called each time you close main menu after save is loaded.

Execute Code When Item Is Equipped/Unequipped[]

Use W3PlayerWitcher events. (scripts\game\player\playerWitcher.ws)

On Equipping Item[]

Use event OnEquipItemRequested

On Unequipping Item[]

Use event OnUnequipItemRequested

Fast Travel And Teleporting Methods[]

You will use mostly CCommonGame methods.

Fast Travel To Location With Specified Position[]

Use method ScheduleWorldChangeToPosition().

import final function ScheduleWorldChangeToPosition( worldPath : string, position : Vector, rotation : EulerAngles );

You need to provide:

World path, which you can get using CWorld method GetDepotPath()

import final function GetDepotPath() : string;

Position, which you can get using CNode method GetWorldPosition()

import final function GetWorldPosition() : Vector;

Rotation, which you can get using CCameraDirector method GetCameraRotation() or set to zero using global function EulerSetZeros()

import final function GetCameraRotation() : EulerAngles;
function EulerSetZeros( out eulerAngles : EulerAngles )
{
	eulerAngles.Yaw		= 0.0f;
	eulerAngles.Pitch	= 0.0f;
	eulerAngles.Roll	= 0.0f;
}

Example[]

Example class with methods for saving worldPath/CameraRotation and traveling to location.

class Example
{
	var worldPathSaved : string;
	var playerPositionSavedSaved : Vector;
	var cameraRotationSavedSaved : EulerAngles;
	var rotationSet : bool; default rotationSet = false; \\to check if rotation was set
	
	public function SaveworldPath()
	{
		worldPathSaved = theGame.GetWorld().GetDepotPath();
	}
	public function SavePlayerPosition()
	{
		playerPositionSavedSaved = thePlayer.GetWorldPosition();
	}
	public function SaveCameraRotation()
	{
		cameraRotationSaved = theGame.GetWorld().GetDepotPath();
		rotationSet = true;
	}
	public function TravelToWorld()
	{
		if (!rotationSet) \\if rotation wasn't set, set it to zeros
			EulerSetZeros(cameraRotationSaved);
			
		theGame.ScheduleWorldChangeToPosition(worldPathSaved, playerPositionSaved, cameraRotationSaved);
	}
}

Fast Travel To Location With Specified Travel Point[]

import final function ScheduleWorldChangeToMapPin( worldPath : string, mapPinName : name );

Fast Travel To Custom Map Waypoint[]

There is DebugTeleportToPin() method in CPlayer (player.ws) that I modified and used it for mod Custom Fast Travel, it allows you there travelling to custom map waypoint (the green one you can set on map)

I added it as an method of W3PlayerWitcher (playerWitcher.ws) in my mod.

function TravelToPin()
	{
		var mapManager 		: CCommonMapManager = theGame.GetCommonMapManager();
		var rootMenu		: CR4Menu;
		var mapMenu			: CR4MapMenu;
		var currWorld		: CWorld = theGame.GetWorld();
		var destWorldPath	: string;
		var id, area, type				: int;
		var position		: Vector;
		var rotation 		: EulerAngles;
		var goToCurrent		: Bool = false;
		var pinNum	: int;
		var waterDepth	: float;
		
		pinNum = FactsQuerySum("TravelPinId");
		
		rootMenu = (CR4Menu)theGame.GetGuiManager().GetRootMenu();
			
		mapManager.GetUserMapPinByIndex( pinNum, id, area, position.X, position.Y, type );		
		destWorldPath = mapManager.GetWorldPathFromAreaType( area );
			
		if (destWorldPath == currWorld.GetPath() )
				goToCurrent = true;
		
		if ( goToCurrent && FactsDoesExist("TravelPinSet"))
		{
			currWorld.NavigationComputeZ(position, -500.f, 500.f, position.Z);
			if (currWorld.GetWaterLevel(position, true) > position.Z)
				position.Z = currWorld.GetWaterLevel(position, true);
			currWorld.NavigationFindSafeSpot(position, 0.5f, 20.f, position);	

			Teleport( position );
			FactsRemove("TravelPinSet");
			FactsRemove("TravelPinId");

			waterDepth = currWorld.GetWaterDepth( position );
			if( waterDepth == 10000 ) waterDepth = 0;
		
			if (!currWorld.NavigationComputeZ(position, -500.f, 500.f, position.Z) && waterDepth <= 0)		
				AddTimer( 'DebugWaitForNavigableTerrain', 1.f, true );
		}
		else if (destWorldPath == "" || !FactsDoesExist("TravelPinSet"))
			theGame.GetGuiManager().ShowNotification("Map pin not set");
		else
		{
			theGame.ScheduleWorldChangeToPosition( destWorldPath, position, rotation );	
			AddTimer( 'DebugWaitForNavigableTerrain', 1.f, true, , true );	
			FactsRemove("TravelPinSet");
			FactsRemove("TravelPinId");
		}
	}

Thx to NiNaka on Nexus who added some code that teleports the player to the water surface when you travel to map pin set on water. But it only works when you travel to the same location, for traveling to another location, you'll have to use event OnAfterLoadingScreenGameStart for example.

Note that it uses facts system to make sure you only travel to "green" map pin and not the others. So you will have to set "TravelPinSet" and "TravelPinID" facts somewhere, preferably using event  OnUserMapPinSet in CR4MapMenu (mapMenu.ws) so they will be set when you add this pin to the map.

event  OnUserMapPinSet( posX : float, posY : float, type : int, fromSelectionPanel : bool )
	{
		var manager	: CCommonMapManager = theGame.GetCommonMapManager();
		var worldPath : string;
		var realShownArea : EAreaName;
		var area : int;
		var position : Vector;
		var idToAdd, idToRemove, indexToAdd : int;
		var realType : int;
		
		idToAdd = 0;
		idToRemove = 0;
		
		if ( m_currentArea == m_shownArea )
		{
			worldPath = theGame.GetWorld().GetDepotPath();
			realShownArea = manager.GetAreaFromWorldPath( worldPath, true );
		}
		else
		{
			realShownArea = m_shownArea;
		}
		
		position.X = posX;
		position.Y = posY;
		position.Z = 0;
		
		realType = type;
		if ( fromSelectionPanel )
		{
			realType += 1;
		}
		if ( !manager.ToggleUserMapPin( (int)realShownArea, position, realType, fromSelectionPanel, idToAdd, idToRemove ) )
		{
			showNotification( GetLocStringByKeyExt("panel_hud_message_actionnotallowed") );
		}
		
		
		if ( idToRemove != 0 && !fromSelectionPanel)
		{
			m_fxRemoveUserMapPin.InvokeSelfOneArg( FlashArgUInt( idToRemove ) );
			//here
			FactsRemove("TravelPinId"); 
			FactsRemove("TravelPinSet"); 
			//here
		}
		else
		{
			m_fxRemoveUserMapPin.InvokeSelfOneArg( FlashArgUInt( idToRemove ) );
		}
		if ( idToAdd != 0 )
		{
			indexToAdd = manager.GetUserMapPinIndexById( idToAdd );
			if ( indexToAdd >= 0 )
			{
				UpdateDataWithSingleUserMapPin( indexToAdd );
				theSound.SoundEvent("gui_hubmap_mark_pin");
			//here
			if ( fromSelectionPanel && !FactsDoesExist("TravelPinSet"))
			{
				FactsRemove("TravelPinId"); 
			}
			else if ( !fromSelectionPanel )
			{
				FactsAdd("TravelPinId", indexToAdd, -1);
				FactsAdd("TravelPinSet"); 
			}
			//here
		}
	}

Teleport To Position In The Same location[]

Use CEntity Teleport() method.

import final function Teleport( position : Vector );

Example:

thePlayer.Teleport(6,6,6);

Enabling Fast Travel From Anywhere[]

Use CCommonMapManager DBG_AllowFT() method. In popular mod Fast Travel To Anywhere it's handled in different way, but this more "ellegant".

function DBG_AllowFT( allow : bool )
	{
		m_dbgAllowFT = allow;
	}

Example:

theGame.GetCommonMapManager().DBG_AllowFT( true );

Preventing "Falling Under The Map"[]

Useful Exec Functions[]

You can call them from console.

Show current position in HUD notification and save it in log. Witcher 3 Scripts Logging

exec function Position()
{
	var position : string;
	position = VecToString(thePlayer.GetWorldPosition());
	thePlayer.ShowHudNotification(position);
	LogChannel('SAVEDPOSITION', position );
}

Show current world path in HUD notification and save it in log.

exec function WorldPath()
{
	var worldPath : string;
	worldPath = theGame.GetWorld().GetDepotPath();
	thePlayer.ShowHudNotification(worldPath);
	LogChannel('SAVEDWORLDPATH', worldPath );
}

List Of World Paths[]

So you won't have to get them manually.

  • "levels\wyzima_castle\wyzima_castle.w2w" - Castle In Vizima
  • "levels\novigrad\novigrad.w2w" - Novigrad/Velen/Oxenfurt
  • "levels\skellige\skellige.w2w" - Skellige
  • "levels\kaer_morhen\kaer_morhen.w2w" - Kaer Morhen
  • "levels\prolog_village\prolog_village.w2w" - White Orchard
  • "dlc\bob\data\levels\bob\bob.w2w" - Toussaint
  • "levels\island_of_mist\island_of_mist.w2w" - Island Of Mist
  • "levels\the_spiral\spiral.w2w" - World from "Through Time And Space" quest.

Items[]

You will use mostly CInventoryComponent methods. To access it you will probably have to use CGameplayEntity method GetInventory().

Spawn Item In Player Inventory[]

Use AddAnItem() method.

public final function AddAnItem(item : name, optional quantity : int, optional dontInformGui : bool, optional dontMarkAsNew : bool, optional showAsRewardInUIHax : bool) : array<SItemUniqueId>
	{
		var arr : array<SItemUniqueId>;
		var i : int;
		var isReadableItem : bool;
		
		
		if( theGame.GetDefinitionsManager().IsItemSingletonItem(item) && GetEntity() == thePlayer)			
		{
			if(GetItemQuantityByName(item) > 0)
			{
				arr = GetItemsIds(item);
			}
			else
			{
				arr.PushBack(AddSingleItem(item, !dontInformGui, !dontMarkAsNew));				
			}
			
			quantity = 1;			
		}
		else
		{
			if(quantity < 2 ) 
			{
				arr.PushBack(AddSingleItem(item, !dontInformGui, !dontMarkAsNew));
			}
			else	
			{
				arr = AddMultiItem(item, quantity, !dontInformGui, !dontMarkAsNew);
			}
		}
		
		
		if(this == thePlayer.GetInventory())
		{
			if(ItemHasTag(arr[0],'ReadableItem'))
				UpdateInitialReadState(arr[0]);
			
			
			if(showAsRewardInUIHax || ItemHasTag(arr[0],'GwintCard'))
				thePlayer.DisplayItemRewardNotification(GetItemName(arr[0]), quantity );
		}
		
		return arr;
	}

Example, adding 1 Clearing Potion:

thePlayer.GetInventory().AddAnItem('Clearing Potion');

Check If Player Has Item In Inventory[]

Use HasItem() method to search for item by name.

Example, checking if player has Clearing Potion:

thePlayer.GetInventory().HasItem('Clearing Potion');

This methods looks for an item by name, there are other methods to search by id or tag, see CInventoryComponent article.

Check If item Is Equipped In Slot By Item Name[]

Use CInventoryComponent method GetItemEquippedOnSlot().

public function GetItemEquippedOnSlot(slot : EEquipmentSlots, out item : SItemUniqueId) : bool
	{
		var player : W3PlayerWitcher;
			
		player = ((W3PlayerWitcher)GetEntity());
		if(player)
		{
			return player.GetItemEquippedOnSlot(slot, item);
		}
		else
		{
			return false;
		}
	}

As you can see you need to pass it a slot (EEquipmentSlots, see enums) you want to check as first argument, and item id (SItemUniqueId) will be stored in variable passed as second argument.

Example, check if player has equipped Wolf School steel sword in EES_SteelSword slot and return bool value:

function Example() : bool
{
	var sword : SItemUniqueId;
	var swordName : name;
	GetInventory().GetItemEquippedOnSlot(EES_SteelSword, sword);
	swordName = GetItemName(sword);
	if (swordName == 'Wolf School steel sword')
		return true;
	else
		return false;
}

Note that you have to use GetItemName() method to get item name by SItemUniqueId.

See CInventoryComponent for more methods used to manipulate items in game.