Difference between revisions of "Development:Quests"
m (→Python Scripting) |
m |
||
Line 1: | Line 1: | ||
− | |||
− | |||
− | This page is | + | This page explains the structure of a quest file, it's integration and internal workings and is useful for campaign/mission/quest programmers. |
=Writing Quests and Adventures= | =Writing Quests and Adventures= |
Revision as of 13:08, 21 April 2008
This page explains the structure of a quest file, it's integration and internal workings and is useful for campaign/mission/quest programmers.
Writing Quests and Adventures
Honestly you merely need a basic understanding of python in order to craft your own add on adventures. But I decided to write a framework for consistently and speedily adding adventures to the general exploration of Vega Strike.
I will define "adventure" as follows: A miniature mission that gets triggered by a player who goes in its system. This mission may either be persistent or non-persistent. A persistent mission will reload the next time the triggering player launched it until the player beats the mission.
THE "QUEST" CLASS: THE ACTUAL QUEST LOGIC GOES HERE
The quest
, quest_factory
and lastly the adventure
modules take care of most of the dirty work. All an quest class has to do is the following:
class quest_my(quest.quest): def __init__ (self): #do anything you need here def Execute (self): #do anything you need here...return 1 if you wish to execute again, 0 if you wish to #terminate yourself
That's it.... though there are some useful functions you may wish to call in your superclass
self.removeQuest() #this prevents your player from EVER encountering the quest again in his life self.makeQuestPersistent() #this causes the quest to be loaded the next time your player jumps in after rebooting vegastrike
Generally before returing 0
(terminating self
) in a mission you may wish to make the quest either persistent (comes back next time he starts game) or removed. Though you could also leave the quest the way it is and the next time the player goes to the system after rebooting vegastrike he'll get the same quest again :-) But in order for your quest to be complete you must have two more components
THE "QUEST FACTORY" CLASS: A CLASS THAT RETURNS THE QUEST NEEDED
Each quest must have a factory that inherits from quest.quest_factory
. and must define both an init, and a create function. Optionally there's a conditional function called precondition.... That returns a boolean whether or not the quest is ripe for creation (when a player encounters it in its native system that is)
class quest_my_factory (quest.quest_factory): def __init__ (self): quest.quest_factory.__init__ (self,"quest_my") def create (self): return quest_my() def precondition(self,playernum): return Director.getSaveData(playernum,"talked_to_sandra",0)>0
The factory must have 2 and can have all 3 of the above functions:
- is the init function (
__init__
) .. you must call your superclass's init with your name. - is the create function... this must simply return the quest class you painstakingly created above
- is the precondition. This is optional (by default it returns 1...always true)
- the precondition can look in the save variables and see if you have done a task (set by some other quest perhaps) and only then return
1
... :-) in that case when a player encounters it for the first time it will check the precondition before launching the quest. Note the precondition does NOT get checked once the mission has been turned into a persistent mission. This is because the mission already made the decision to make itself persistent I don't expect many missions to need to be persistent. I expect more lighthearted adventures that don't drag out if a player quits, etc.
- the precondition can look in the save variables and see if you have done a task (set by some other quest perhaps) and only then return
Anyhow... a good example is the persistent quest known as the quest_drone
(quest_drone.py
) That quest checks to see if you're near an unknown_derelect and if so it launches the badguy drone.... it makes the drone jump and follow you wherever you go until it is destroyed...it is persistent and launches the drone every time you rerun the game. Luckily if you're close to the derelect chances are you'll find the gun that you can use to kill the drone.... which brings me right smack into my next point: location location location!
THE LOCATION AND PERSISTENCE OF A GIVEN QUEST (i.e. HOOKING IT INTO privateer.py)
adventure.py has the master list of all possible quests in Vegastrike. Right now there are two (but one is stupid and will be removed soon enough...that's the default one)
adventures = {"gemini_sector/delta_prime":quest_drone.quest_drone_factory(), "sol_sector/celeste":quest.quest_factory("default_quest",0)}
persistent_adventures = [[quest_drone.quest_drone_factory()]
These are the only 2 lists that will ever need to be changed in the modules that are already there.
-
adventures
contains a map of location to adventure. Note that there can be only 1 adventure per location (the players need to be encouraged to actually EXPLORE) - The second list (
persistent_adventures
) lists all possible "persistent adventures" (duh; though they are only loaded from if the given adventure has gotten around to callingself.makeQuestPersistent
)
Anyhow just adding more cool missions to this list will make it a lot more fun to explore around and encounter strange people, strange news, and a bunch 'o cash :-P
Note that we should eventually make the news reports line up with the quests... :-) I have some ideas how to do that. Basically you can call
VS.IOmessage ("game","news","A drone was sighted in the delta prime...blah blah blah")
and then people will see it in the game when they click on GNN likewise if you send it to
VS.IOmessage ("game","bar","A drone was sighted in the delta prime...blah blah blah")
then you can hear it when you talk to the bartender :-)
Python Scripting
Scripting Basics
VS has a lot of different ways of doing one thing. Just pick the most appropriate one for the job. It supports campaigns, missions, independent fixers, quests, adventures, etc., all of them having their specific ways of hooking them up into the game.
The python code resides in the directory data4.x/modules.
Quests & Adventures
Each script requires at least those 2 elements:
-
def __init__ (self):
- here you initialize your script variables -
def Execute (self):
- this part is executed each simulation frame; this is the place for writing the actions of the adventure.
Quests are run inside of "random_encounters.py". If it returns 1 or True, it will keep going. If it returns 0, False or None (python returns None if you don't say what to return) then the quest will stop Execute()ing and be deleted until you jump into that system again or reload.
Most of the python bindings (to the c++ engine) are listed in data4.x/modules/stub/VS.py
and commented in HowTo:Edit_Missions:Python:Bindings:Python.
There are many other useful functions coded in python and distributed through various other classes.
-
data4.x/modules/Vector.py
contains vector manipulation functions -
data4.x/modules/universe.py
has functions related to the current and adjacent systems -
data4.x/modules/unit.py
provides some unit properties, methods, and actions -
data4.x/modules/launch.py
is useful for spawning units in almost any place -
data4.x/modules/faction_ships.py
allows for a fine creation of ship lists by type and faction, e.g. for spawning.
Some of the functions that might be of help when writing quests and adventures will be discussed further down.
Basic Quest Setup
This is how your basic empty quest file should look like (replace questname with an individual quest name):
import quest
class quest_questname (quest.quest):
def __init__ (self):
self.player = VS.getPlayer()
def Execute (self):
return 1
In order to integrate the quest into the adventure file, you'll need to also include factory class (again, replace questname with an individual quest name):
class quest_questname_factory (quest.quest_factory):
def __init__ (self):
quest.quest_factory.__init__ (self,"quest_questname")
def create (self):
return quest_questname()
And in the adventure file, add the information required to initiate the quest (replace questname with an individual quest name) to the adventures list including the sector/system names where the quest should be initiated and the call to the your quest factory class:
import quest_questname
adventures = {
"Crucible/Cephid_17":quest_questname.quest_questname_factory(),
}
Now, that you have the main classes set up, you are ready to start writing the quest contents.
Getting Objects
FIXME Improve/expand it.
In the script you will require to reference the objects of the universe. Here are some examples to get you started:
-
self.player = VS.getPlayer()
- that's you -
self.planet = unit.getUnitByName('Atlantis')
- get planet Atlantis -
self.station = unit.getUnitByName('MiningBase')
- get the mining base -
self.vessel= unit.getUnitByName('Ox')
- get an Ox ship -
self.gf = VS.launch("Charlotte the Harlot","Robin","privateer","unit","default",1,1,(1000,10,3000),)
- creates one ship of type "Robin", being of the faction "privateer", at position (1000,10,3000), which will behave like "default" and show up in the HUD as "Charlotte the Harlot"
Getting Properties
Once you have referenced the appropriate object, you want to get information about it. Again, some commented examples follow:
-
self.starttime = VS.GetGameTime()
- get the time since the start of the game. This is not the universe time (star date). -
self.planet.isDocked(self.player)
- verify if the player is docked to the planet. Note that you need to 'read' the meaning from right to left in this case. -
self.station.getDistance(self.player)
- The distance to the center of an object. Note that when landed on a planet, the distance is the actual planet radius. -
vec = self.player.Position()
- assign the player's position vector to a variable
Communications
Sending messages through the comm is fairly easy:
-
VS.IOmessage (0,"Oxen","all","Hi buddy.")
- make the vessel send a message to all through the comm immediately (the first 0) when the script line is executed. -
VS.IOmessage (10,"You","all","Do I know you?")
- makes the player send a public message through the comm 10 seconds after the script line is executed.
You can even set your own color for the messages with a html color code:
-
VS.IOmessage (20,"Oxen","You","#8080FFYou don't remember me?")
- prints the comm message in light blue.-
self.txtColor="#8080FF"
-
VS.IOmessage (30,"Oxen","Privateer",self.txtColor+"I can hardly believe this")
- you can even define a color variable and add it to each line.
-
As far as the communications go, there is a variable in cockpit that holds what the VDU screen shows as possible options. Unfortunately the communications system is not scriptable at the moment.
Comm animations can be called from your quest script and will be shown on the Comm VDU:
-
self.player.commAnimation("com_questname_animationname.ani")
The animation files must be placed in a corresponding folder data4.x/animations/com_questname_animationname.ani
and the animation definition file named equally com_questname_animationname.ani.
More Topics
FIXME Improve/expand it.
Flight Groups
You can use "flightgroup directives" to give units specific orders (which is cool if you want to generate two drones and make one blow up another). The list of directives is in C++ (undocumented).
A capital letter overrides the AI script, while a lower case letter will allow it to be changed. You can also add a "." to the end to ensure that the unit will not change targets on its own.
The following directives are available:
- H, h (help) -- leader of flightgroup requests help.
- B, b (break and attack) -- default, everyone attacks like normal
- A, a (attack target) -- attack their current targets
- k -- something undocumented, says something about capships. Probably it's an order to take out capship turrets with priority, or it's a pure bomber order to attack capships and ignore fighters.
- F, f (flagship?) -- Ship being escorted. I would imagine that it's an order to attack a ship being escorted by the hostile faction. Alternatively, it could be an order to fly in formation.
- L, l -- undocumented, like F but has complicated formation rules. Could be a shortcut for "Leave" - ships heading out.
- E, e (escort) -- half of this is copy and pasted from L but I have no idea. I'd say it is "escort", escort a designated ship.
- P, p (protect) -- I want to help out
Unfortunately I don't see any codes for acting like a sitting duck... You could try something like making it target itself, or making it target NULL (which you get by calling the constructor VS.Unit() ) and then using something like "A.". Or, just try doing a "B." and having it target you, and make sure it is friendly... then it might just fly near you. I believe you can get the flight group to fly to places by having it target them and then force it to do Unit.setFgDirective("B.") which I think forces it to act like normal and fly to this target.
For some directives to work, you must set the appropriate target and adjust the relation between the factions to either hostile (e.g. attack) or friendly (escort, protect). Here's a snippet to get you started:
self.aggressor.SetTarget(self.victim)
VS.AdjustRelation(self.aggressor.getFactionName(),self.victim.getFactionName(),-1,-1)
self.aggressor.setFgDirective("A.")
Quest Testing
In order to speed up the scripting and testing for adventures that are not already Director.Missions, you can create a mission xml file and call the adventure file directly. In order for it to work properly, it must inherit the Director.Mission class *and* call its constructor. Unfortunately quests must inherit from the quest class, so you need to use a wrapper class that takes an instance of a class as an argument:
import Director
class Executor(Director.Mission):
def __init__(self, classesToExecute):
Director.Mission.__init__(self)
self.classes = classesToExecute
def Execute(self):
for c in self.classes:
c.Execute()
This is a very simple class, and it's similar to what privateer.py does. I'm just making it general by passing the list of classes as an argument.
Then, in the mission xml file, you need to call that class by adding the python section:
<python>
from quest_tutorial import Executor, quest_tutorial
tmpvar=Executor([quest_tutorial()])
</python>