This English version is
only for G2. Why? Because there's already an English version published on RPGDot, but it's only for G1:
[Script Tutorial] (Translated by Alistair)
The Gothic 2 parts of this tutorial were translated from the German tutorial by wWw
"Gunther" Tutorial
Contents:
- [Introduction]
- [The quest-setting character]
- [Gunther's daily routine]
- [The quest]
- [The object of the quest]
- [The "Quest completed" dialogue]
1. Introduction
At the end of this tutorial, you'll have produced the elements of a mini-mission, where the player has to retrieve a sword for a non-player character. You'll create the quest-setting NPC, the sword, and the necessary dialogues.
You'll need the Gothic Mod Devlopment Kit, and a text editor. UltraEdit is the editor recommended by Piranha Bytes.
Note that this tutorial doesn't leave you with a stand-alone Mod, it involves modifying actual Gothic game code. Once complete, your new NPC will be part of your Gothic game, albeit not one the other characters are very interested in.
Anything written after a // is a comment. If a comment continues onto a second line, you need to start each line with //, to prevent Gothic from treating it as code.
2. The quest-setting character
To add a new NPC to the game, you need to create a new instance of the class C_Npc. (Bet you wish you'd paid attention in those C++ classes now.) What follows is a short overview of the class, which we'll then fill out with the values for our new NPC.
The class:
CLASS C_NPC
{
VAR INT id; // an ID number for the NPC
The id is a unique identifier for this NPC. An example of its use is matching the right daily routine to the right NPC.
VAR STRING name [5]; // The NPC's name
This string is the one shown when the NPC is in focus or is speaking.
VAR STRING slot;
This string is unused at present, but is available for any special situations that might come up.
VAR INT npcType;
This integer identifies particular groups of characters within Gothic. For example Npc_TypeMain refers to an important plot character such as Diego.
VAR INT flags;
Used to set the NPC as immortal, with flags = NPC_FLAG_IMMORTAL;
VAR INT attribute [ATR_INDEX_MAX];
The attribute values of an NPC are stored in an array - more on this later.
VAR INT protection [PROT_INDEX_MAX];
The protection variable is handled in the same way as the Attributes above. They can be used to give an NPC a natural armor against certain damage types, e.g. fire.
VAR INT damage [DAM_INDEX_MAX];
Theoretically, this gives the ability for an NPC to do differing amounts of damage in melee combat. In practice it's a bit pointless, because damage is usually governed by the weapon-type. Should you want to experiment, acceptable values are:
- DAM_INDEX_BLUNT
- DAM_INDEX_EDGE
- DAM_INDEX_POINT
- DAM_INDEX_FIRE
- DAM_INDEX_FLY
- DAM_INDEX_MAGIC
VAR INT damagetype;
Gives the type of damage done by the NPC, unless a wepaon is used.
VAR INT guild, level;
Used to assign the NPC a particular guild and level. A typical Guild might be GIL_NONE, for those with no guild.
VAR FUNC mission [MAX_MISSIONS];
Obsolete.
var INT fight_tactic;
This determines how the NPC will act in combat. If you're lucky, there will be further discussion of FAIs (Fight AIs) later on.
VAR INT weapon;
Obsolete.
VAR INT voice;
Voice number for the NPC.
VAR INT voicePitch;
Can be used to alter the voice's pitch.
VAR INT bodymass;
Obsolete.
VAR FUNC daily_routine;
This describes which daily routine this character is to use.
VAR FUNC start_aistate;// Here you can give the 'state' which is used when the character is first created.
// **********************
// Spawning
// **********************
VAR STRING spawnPoint;
This holds a waypoint where the NPC will respawn when killed. If left empty, death is final.
VAR INT spawnDelay;
Delay (in real-time seconds) between death and respawning.
// **********************
// SENSES
// **********************
VAR INT senses;
NPCs have three senses:
- SENSE_SEE. This is real sight, ie obstacles block it.
- SENSE_HEAR. Walls block this, but trees, for example, don't. NPCs can also hear behind them.
- SENSE_SMELL. Nothing blocks the mighty sense_smell!
VAR INT senses_range;
Range of senses in cm.
// **********************
// Feel free to use
// **********************
VAR INT aivar[50];
An array of 50 variables which can be stored for each NPC.
VAR STRING wp;
C++ Code (in Gothic.exe) stores whichever waypoint here which is currently associated with the NPC, usually the next one he'll head to, given his daily routine.
// ***********
// Experience
// ***********
VAR INT exp; // Experience points
VAR INT exp_next; // Experience points needed to reach next level
VAR INT lp; // Learning points
};
These three variables are only useful for the player character.
So now let's use this template to create a real Gothic character. We'll call him Gunther - he'll not be a member of any guild and will be an 'ambient NPC'. He's a bit of a bruiser, Level 17, Strength 100 and 200 HP. No mana, though. And he's deaf, why not. His other senses have a 20m range. He'll also have 40% critical hit chance with one-handed weapons. We'll put an appropriate weapon in his inventory. So here goes. You need to produce (or paste) this text in a text editor...
instance None_999_Gunther (Npc_Default) // Could also be C_Npc instead of Npc_Default – it doesn’t matter.
{
//-------- primary data --------
name = "Gunther";
guild = GIL_NONE;
npctype = NPCTYPE_AMBIENT;
level = 17;
voice = 8;
id = 999;
// ----- attributes --------
attribute[ATR_STRENGTH] = 100;
attribute[ATR_DEXTERITY] = 100;
attribute[ATR_MANA_MAX] = 0;
attribute[ATR_MANA] = 0;
attribute[ATR_HITPOINTS_MAX]= 200;
attribute[ATR_HITPOINTS] = 200;
//-------- visuals --------
B_SetNpcVisual //A function to create the visual for the NPC. Each gender has different Body- and Head- meshes.
//The NPC will be automatically scaled as well.
(self,
MALE, //Gender
"Hum_Head_Fatbald", //Head mesh
51, //Head texture
0, //Body texture
ITAR_MIL_M); //Armor instance
Mdl_ApplyOverlayMds (self,"Humans_Militia.mds"); // overlay animation file & movement type - military
Mdl_SetModelFatness (self, 0); // limb fatness
//-------- talents --------
//B_SetFightSkills (self, 40); //Here the skills are all automatically set to 40%.
//We don't want this, but just for reference ;)
//Manually you can set the skills individually.
HitChance [NPC_TALENT_1H] = 40;
HitChance [NPC_TALENT_2H] = 0;
HitChance [NPC_TALENT_BOW] = 0;
HitChance [NPC_TALENT_CROSSBOW] = 0;
//-------- inventory --------
EquipItem (self, ItMw_1h_Mil_Sword);
// Ten heal-potions in case he gets into a scrape:
CreateInvItems (self, ItPo_Health_01, 10);
//--------senses-------------
senses = SENSE_SEE | SENSE_SMELL;
senses_range = 2000; //2000 cm = 20 m
//------------- ai -------------
fight_tactic = FAI_HUMAN_STRONG;
//Reference to the daily routine. It will be further elaborated upon later.
daily_routine = Rtn_start_999;
};
//And even when it still has to be explained later, the function has to be here just so that everything works:
func void Rtn_Start_999 ()
{
};
Save this text file in /_work/data/scripts/content/story/Npc, called something like Gunther.d. The .d extension means it will be picked up when Gothic comes to parse its scripts.
Now we need Gothic to recognize the existence of this new instance of the NPC class. We either do this by starting the Spacer and choosing World/Reparse Scriptfiles... or by choosing 'Reparse all scripts' when we start Gothic from GothicStarter. (You only have to do this once.) I find using the Spacer more reliable, albeit slower.
Once this is done, Gunther can be introduced into the game world from the console, with the command Insert None_999_Gunther. Doesn't do much yet, does he. That will now change... If you can't insert Gunther from the console, something's gone wrong, and you should sort it out before proceeding.
3. Gunther's daily routine
To give the NPC something to do while he waits for you, he 'll need either a Daily Routine or a start state. Gunther's daily routine will involve standing in a particular place, and occasionally taking a leak.
This simple NPC is only going to react to the approach of the player. He won't have especially sophisticated reactions to other things, like attacks etc.
Here's the code:
func void ZS_GuntherWait ()
{
PrintDebugNpc (PD_TA_FRAME, "ZS_GuntherWait"); // Debug output, which is picked up by the ZSpy application.
Npc_PercEnable(self, PERC_ASSESSPLAYER, B_AssessPlayer); // Amongst other things, checks whether this NPC needs to speak to the player on approach.
Npc_PercEnable(self, PERC_ASSESSTALK, B_AssessTalk); // Means you can talk to the NPC, for example to end the quest.
AI_StandUp (self);
AI_SetWalkmode (self, NPC_WALK);
AI_GotoWP (self, self.wp); // Go to the start point of your Daily Routine
AI_AlignToWP (self); // Face in the same direction as the waypoint arrow
};
func void ZS_GuntherWait_Loop ()
{
PrintDebugNpc (PD_TA_LOOP, "ZS_GuntherWait_Loop"); // More debug output
AI_GotoWP (self, self.wp); // Go to the Daily Routine start point.
AI_Wait (self, 100); // Wait 100 seconds here
AI_GotoWP (self, Npc_GetNearestWP (Self)); // Look for another nearby waypoint
AI_PlayAni(self, "T_PEE");
AI_Wait (self, 100); // Wait another 100 seconds before the loop ends
};
func void ZS_GuntherWait_End ()
{
PrintDebugNpc (PD_TA_FRAME,"ZS_GuntherWait_End");
};
So, nearly ready. We just have to include this Daily Routine in the list of such routines. To do this, we open /_work/data/skripts/content/story/ZS/TA.d and insert these lines:
func void TA_GuntherWait (var int start_h,var int start_m,var int stop_h,var int stop_m,var string waypoint)
{
TA_Min (self, start_h,start_m, stop_h, stop_m, ZS_GuntherWait, waypoint);
};
Then save TA.d. Now these lines:
func void Rtn_Start_999 ()
{
TA_GuntherWait (0,00,13,00, "NW_XARDAS_TOWER_03");
TA_GuntherWait (13,00,0,00, "NW_XARDAS_TOWER_03");
};
This ensures that Gunther will stand in front of Xardas' tower all day long.
NB: Daily Routines (which often start TA_, from the German 'Tagesablauf') need at least two periods, as above. Otherwise, the engine gets confused. They get saved with some filename, in /_work/data/skripts/content/story/ZS, with a .d extension.
One final step, and Gunther is in the game. Open /_work/data/skripts/content/story/Startup.d and insert this:
Wld_InsertNpc(None_999_Gunther,"NW_XARDAS_TOWER_03");
in the block which refers to Xardas' Tower. And there we have it. Reparse the scripts, as you did before, and Gunther is in the game.
4. The Quest
Quests are implemented in Gothic through the dialogue system. We'll go through the basic structure of a dialogue, and then fill it out with the info which describes the quest to find the sword and hand it over.
The code:
instance Instance_Name (C_INFO)
{
npc = NPC_Name; // This is the NPC whose dialogue this is.
condition = Instance_Name_Condition; // This points to a function which assesses whether this dialogue is to be triggered
information = Instance_Name_Info; // This points to a function which implements the actual dialogue.
important = 1; // Identifies dialogues which the NPC will start up, without the player talking to the NPC first.
permanent = 0; // Permanent dialogues are always offered
};
FUNC int Instance_Name_Condition()
{
return 1;
};
The function evaluates to True or False (or 1 or 0). When true, the NPC will offer the dialogue. Here it is always true.
func void Instance_Name_Info()
{
// This function will hold the actual dialogue - explained in more detail below..
};
In addition, NPC dialogues always have an Exit function, which puts the 'End' in the dialogue options.
// ************************ Exit function **************************
instance Instance_Name_Exit (C_INFO)
{
npc = NPC_Name;
condition = Instance_Name_Exit_Condition;
information = Instance_Name_Exit_Info;
important = 0; // Only presented if the player speaks to the NPC
permanent = 1; // Does not go away if chosen in earlier dialogues. description = "END";
};
FUNC int Instance_Name_Exit_Condition()
{
return 1; // Always needed...
};
FUNC VOID Instance_Name_Exit_Info()
{
AI_StopProcessInfos (self);
};
So that's it. By including the right values and appropriate functions in these dialogues, quests are created.
Let's go.
Step 1. In _work/data/skripts/content/story/Storyglobals.d add these lines:
const string GunthersSword = "Bring Gunther's sword back";
var int int_GotSword;
These describe the quest in your journal and keep track of whether you've completed the quest.
Save.
Some code:
instance None_999_Gunther_AskForSword (C_INFO)
{
npc = None_999_Gunther;
condition = None_999_Gunther_AskForSword_Condition;
information = None_999_Gunther_AskForSword_Info;
important = TRUE; // Gunther will talk to the player on approach
permanent = FALSE; // The quest is offered only once.
description = ""; // Empty here, as nothing is to appear in the initial dialogue box at the top of the screen.
};
FUNC int None_999_Gunther_AskForSword_Condition()
{
if (hero.level >= 0) // A fake check - only players above level 0 will be offered the quest.
{
return TRUE;
};
return FALSE; // This is Daedelus' IF-THEN-ELSE structure
};
func void None_999_Gunther_AskForSword_Info()
{
AI_Output ( self, other, "None_999_Gunther_AskForSword_Info_8_01"); // Hey you, busy?
// The first number represents the NPC's voice, the second is a counter, identifying each line.
Info_ClearChoices (None_999_Gunther_AskForSword);
Info_AddChoice (None_999_Gunther_AskForSword, "Nope.",
None_999_Gunther_AskForSword_Yes);
Info_AddChoice (None_999_Gunther_AskForSword, "Afraid so.",
None_999_Gunther_AskForSword_No);
};
// If the player says he's busy...
void None_999_Gunther_AskForSword_No ()
{
AI_Output(other,self, "None_999_Gunther_AskForSword_Info_8_02"); // I'm busy
AI_Output(self,other, "None_999_Gunther_AskForSword_Info_8_03"); // Ok, whatever...
AI_StopProcessInfos (self);
};
// If the player says yes...
func void None_999_Gunther_AskForSword_No ()
{
AI_Output(other,self,"None_999_Gunther_AskForSword_Info_8_04"); // Nope... AI_Output(self,other,"None_999_Gunther_AskForSword_Info_8_05"); // Go get ma sword.
Log_CreateTopic (GunthersSword,LOG_MISSION);
Log_SetTopicStatus (GunthersSword,LOG_RUNNING);
B_LogEntry(GunthersSword, "Gunther has rather tersely asked me to get his sword back.");
AI_StopProcessInfos (self);
};
And that's basically it. Gunther asks the player if he's busy, and responds differently according to what he says...
You need to save this dialogue as a file called DIA_something.d, in /_work/data/skripts/content/story/Missions. You also need to create the 'Output Units' files, before your text will actually appear. Delete or move the files OU.bin and OU.csl from whichever Gothic folder they're currently in.
Start the Spacer. Press the button on the horizontal toolbar to bring up the Output Units window. Press Refresh. Press Save.
That's it.
Note: Bear in mind that if you are creating a Mod, you might want to set the parameter enableSubtitles, to ensure people can see the text, given that there's no sound.
5. The Object of the Quest
We're going to create a sword, to be the object of the quest. As with the NPC described above, we'll outline the relevant class, and then fill in a particular instance.
The class:
CLASS C_Item
{
VAR INT id; // As with the C_Npc class above.
VAR STRING name, nameID; //As with C_Npc
VAR INT hp,hp_max; //As with C_Npc
VAR INT mainflag, flags;
VAR INT weight, value; // Integer values
// For Weapons:
VAR INT damageType; //Type of damage
VAR INT damageTotal;
VAR INT damage [DAM_INDEX_MAX];
// For Armor
VAR INT wear;
//Only WEAR_TORSO or WEAR_HEAD can be used.
VAR INT protection [PROT_INDEX_MAX];
// This is an array which stores the protection afforded by the armor against each damage type.
// For foods
VAR INT nutrition; // HP increase on consumption
// When using items...
VAR INT cond_atr [3];
VAR INT cond_value [3];
// An array of two x three integers, used to hold the level of the attribute necessary to use the item.
// The attributes which are changed when the object is used or worn.
VAR INT change_atr [3];
VAR INT change_value [3];
// Parser functions
VAR FUNC magic;
VAR FUNC on_equip;
VAR FUNC on_unequip;
VAR FUNC on_state [4];
// Owner
VAR FUNC owner; // Owner: Instance-Name
VAR INT ownerGuild; // Owner: Guild
VAR INT disguiseGuild; // When item is worn,
//3DS Data
VAR STRING visual; //The location of the visual (mesh) for the object.
// Changing appearances with objects
VAR STRING visual_change ; // ASC - File - Identifies the new visual associated with putting on armor.
VAR STRING effect; // Effect Instance
VAR INT visual_skin; //Texture variation for the affected armor
VAR STRING scemeName; //Internal name for item use
VAR INT material;
VAR STRING pfx; // Magic Weapon PFX (Obsolete)
VAR INT munition; // Instance of Munition
VAR INT spell; // Which spell does the item use
VAR INT range; // The radius which a melee weapon can hit an opponent
VAR INT mag_circle; // Which circle is needed for the spell
//The next three are shown when item is highlighted in the invintory
VAR STRING description;
VAR STRING text[ITM_TEXT_MAX];
VAR INT count[ITM_TEXT_MAX];
};
Now that we've looked at the class, let's make a nice sword for the quest. It should cost 1000 and make damage of 300. The visual will be stolen from the gothic data, because visuals are explained somewhere else. Of course, the sword should be a melee weapon, be a sword, and made from metal.
INSTANCE ItMw_1H_GuntherWantedSword (C_Item)
{
name = "Millhouse Sword";
mainflag = ITEM_KAT_NF; // Melee weapon
flags = ITEM_SWD; // Sword
material = MAT_METAL; // is made of metal
value = 1000; // worth 1000
damageTotal = 300;
damagetype = DAM_EDGE; // Preset sword-damage
range = 200; // Radius of 2m
cond_atr[2] = ATR_STRENGTH; // which attribute is required
cond_value[2] = 5; // the min of the required attribute
visual = "ItMw_020_1h_sword_old_01.3DS"; // Visual from the gothic files
description = name;
TEXT[2] = "Damage"; COUNT[2] = 300; //Text und Values in the first row
TEXT[3] = "Strength needed"; COUNT[3] = cond_value[2]; //Text und Values in the second row
TEXT[4] = "One-handed"; //Text in the third row
TEXT[5] = "Value"; COUNT[5] = value; //Text und Values in the forth row
};
The sword can be inserted exactly the same way as Gunter, by using the console.
But first, you will need to make sure that the sword instance is in a *.d file of it's own, and that the file is in the following folder: /_work/data/skripts/content/Items. This time you actually have to make sure that the engine will read the file. So open the file: /_work/data/skripts/content/gothic.src. the gothic.src file contains all of the file paths, which the engine can then read. The paths actually start in /_work/data/skripts/, so all that you have to do is insert your new path: ITEMS\
FILENAME.D
Watch out though.
Cases are very important in this document. So watch out. If you don't, it could corrupt the entire document.
And of course you can play around an reference entire new folders if you want. :)
6. The 'Quest Complete' Dialogue
This is the dialogue that is triggered if the quest conditions have been met.
// ************************ EXIT **************************
instance None_999_Gunther_AskForSword_Exit (C_INFO)
{
npc = None_999_Gunther;
condition = None_999_Gunther_AskForSword_Exit_Condition;
information = None_999_Gunther_AskForSword_Exit_Info;
important = 0;
permanent = 1;
description = "END";
};
FUNC int None_999_Gunther_AskForSword_Exit_Condition()
{
return 1; // Always has the option to choose End
};
FUNC VOID None_999_Gunther_AskForSword_Exit_Info()
{
AI_StopProcessInfos ( self );
};
instance None_999_Gunther_BringSword (C_INFO)
{
npc = None_999_Gunther;
condition = None_999_Gunther_bringSword_Condition;
information = None_999_Gunther_bringSword_Info;
important = FALSE;
permanent = TRUE;
description = "Do you have the sword?";
};
FUNC int None_999_Gunther_bringSword_Condition()
{
// 1. If the player accepted the quest…
// 2. and hasn't yet completed it…
// 3. and has the sword…
// … offer the dialogue
if (Npc_KnowsInfo ( hero, None_999_Gunther_AskForSword) // (1)
&! int_GotSword // (2)
&& Npc_HasItems (other, ItMw_1H_GuntherWantedSword) >= 1) // (3)
{
return TRUE;
};
return FALSE;
};
func void None_999_Gunther_bringSword_Info()
{
Info_ClearChoices (None_999_Gunther_bringSword);
Info_AddChoice (None_999_Gunther_bringSword,
"I've got it and I'm keeping it!",
None_999_Gunther_bringSword_Yes);
Info_AddChoice (None_999_Gunther_bringSword,
"Here you go, mate.",
None_999_Gunther_bringSword_No);
};
// If the player doesn't hand it over…
func void None_999_Gunther_bringSword_Yes ()
{
AI_Output( other, self, "None_999_Gunther_bringSword_Info_8_06"); // I don't think so, sonny.
AI_Output( self, other, "None_999_Gunther_bringSword_Info_8_07"); // Then take this!
AI_StopProcessInfos (self);
AI_StartState( self, ZS_Attack, 0, ""); // Gunther attacks
};
// The player hands it over
func void None_999_Gunther_bringSword_No ()
{
AI_Output ( other, self, "None_999_Gunther_bringSword_Info_8_08"); // Here you go.
AI_Output ( self, other, "None_999_Gunther_bringSword_Info_8_09"); // Triffic.
B_LogEntry (GunthersSword, "Gave him his sword");
other.exp = other.exp + 100; // +100 experience points
other.lp = other.lp + 10; // … and some learn points
//Npc_GiveItem ( other, MilhouseSchwert, self); // Gunther gets the sword
AI_StopProcessInfos (self);
};
So that's it. Just put that in with the rest of the dialog, which you saved earlier, and you're finished. Congratulations!