Missions and NPC Interaction - Thargoid/pioneer GitHub Wiki
Non-player characters can be defined and stored easily using the Character class. Character provides an object which represents a simple character sheet, along with a set of methods with which to operate on it.
Persistent characters
Missions have always used characters, to a certain degree. Bulletin board missions always display a face and a name, and that name will often be recorded on the current mission list. Traditionally, though, there has been no mechanism by which a character can be stored, and retrieved by a later mission, perhaps one that is not related at all.
NPCs defined using the Character
class retain their name, gender, physical
appearance and so forth. They can be used during the course of a mission
script, and when they are no longer required they can be saved into a pool of
reusable characters.
Stored in the character's object are several values. By default, the character has four personality attributes, and four skills attributes. More can be added to any character at any time by a script.
Characters do not have to be persistent; they can be created and discarded for single missions very easily. If stored as persistent characters, then they immediately become available for other missions to use.
Creating characters
Characters are created using the class constructor, Character.New()
. This
returns a character object, complete with a name, gender, face, inherited
default values and inherited methods. Defaults can be specified by passing a
table of values as a parameter.
Selecting characters that already exist
Of course, the whole point of persistent characters is that they are reusable.
All reusable characters are stored in a global array, named
PersistentCharacters
. The Character
class provides two methods for
searching for characters for use, as well as plenty of information to help a
mission script decide between the various characters that might be on offer.
The most useful of these is available
, which is a simple boolean flag
indicating that the character is not currently being used by another mission.
The simplest method for finding characters is Character.FindAvailable()
.
This method returns an iterator function, suitable for use in a Lua for
loop. It returns each character in PersistentCharacters where available
has been set to true. In this example, we look through all the available
charcters, and find the first one whose navigation skill is above 50;
otherwise, we create a new character with the desired attribute.
local SelectedPerson
for PotentialPerson in Character.FindAvailable() do
if PotentialPerson.navigation > 50 then
SelectedPerson = PotentialPerson
break
end
end
SelectedPerson = SelectedPerson or Character.New({navigation=51})
The other method is much more versatile. Character.Find()
returns an
iterator function, in the same way the Character.FindAvailable()
does. By
default it returns all persistent characters. What makes it so much more
powerful, though, is that it can take a Lua function as a parameter. That
function must take a Character
object as an argument, and must return a
boolean value. It will be used by Character.Find()
as the basis for
selection of characters. Using this method, the above example could be written
like this:
local Selector = function (character)
return character.available and (character.navigation > 50)
end
local SelectedPerson
for PotentialPerson in Character.Find(Selector) do
SelectedPerson = PotentialPerson
break
end
SelectedPerson = SelectedPerson or Character.New({navigation=51})
Of course, Selector could be arbitrarily complex, and can be re-used as often
as necessary. Each character has details of the last time that a script
finished using it, as well as the location where that event took place. These
are stored in the lastSavedTime
and lastSavedSystemPath
attributes.
These can come in very useful for selecting characters in a realistic fashion.
Using the character
The first thing to do with a character is to mark it as unavailable, so that
other Lua scripts won't choose to use it. The CheckOut()
method will set
the character's available
flag to false. In addition, it performs some
checking, and returns a boolean value. If it returns true, it is indicating
that the checkout operation was successful, and that another script hadn't
checked the character out in the mean time. The first example above could be
refined to take this into account:
local SelectedPerson
for PotentialPerson in Character.FindAvailable() do
if PotentialPerson.navigation > 50 and PotentialPerson:CheckOut() then
-- This character is ours now!
SelectedPerson = PotentialPerson
break
end
end
SelectedPerson = SelectedPerson or Character.New({navigation=51})
Releasing a character for other scripts to use
Once a character has been finished with by a script, it can be returned (or
added) to the pool of available characters using the Save()
method.
SelectedPerson:Save()
SelectedPerson = nil -- Free up this variable; the character is safely stored
Save()
checks the PersistentCharacters
array for the presence of this
character, and if it isn't there, it inserts the character into that array.
It then sets available
to true, and updates the lastSavedTime
and lastSavedSystemPath
attributes.
De-persisting a character
If a character is to be retired for any reason (perhaps unused characters need
to be pruned, or one just suffered death at the hands of the plot) then it can
be removed from the PersistentCharacters
array using the UnSave()
method.
DeadPerson:UnSave()
This removes the character, but does not destroy it. It would be perfectly
possible to re-insert the character with Save()
. Of course, once all other
references to the character have gone, the character is lost completely and
will be destroyed by the Lua garbage collector.
Using a Character object
Each character has a number of attributes and skills. By default there are four personality attributes and four crew skills attributes. More can be added at any time by a script, simply by defining them on the fly.
The default personality attributes are luck
, charisma
, notoriety
and lawfulness
.
Personality attributes
Luck
The luck
attribute should probably be the least used attribute of a
character. It is used to test how fortunate a character is, in the absence of
more appropriate attributes.
Charisma
The charisma
attribute can be used to determine whether a character manages
to win a contract, or reach a favourable agreement with another party. It
reflects the character's confidence, body language and general likeableness.
Notoriety
The notoriety
attribute reflects the manner in which a character's
reputation precedes them. This could mean fame, or infamy, or just being known
for having a good time. It could be combined with lawfulness
, for example;
a notorious and unlawful character is very different from the notorious and
lawful character who is probably trying to apprehend them. Both are likely to
be generally recognised, whereas the non-notorious and unlawful character who
just picked somebody's pocket is not.
Lawfulness
The lawfulness
attribute shows where a character stands regarding the law.
It's not necessarily evidence of criminal behaviour, but whether a character
is likely to uphold the law, or ignore it. A lawful character would be less
likely to accept work as crew on a pirate ship. An unlawful character would
be less likely to take up an honest career when there are more profitable
options.
Player Relationship
The playerRelationship
attribute describes how much the character likes, or gets
on with, the player. A low score means they despise the player, while a high score means
they adore the player.
Crew skills attributes
Engineering
The engineering
attribute is intended to reflect the character’s
mechanical, electrical or other tecnical skills. An engineer can install
equipment on a ship, repair minor damage and keep the ship running smoothly.
Piloting
The piloting
attribute is intended to reflect the character’s skill at flying spacecraft. Above a certain value, the game could use this stat to allow a
ship crewed by this character to fly on autopilot, even when one is not fitted.
Navigation
The navigation
attribute is intended to reflect the character’s skill at
course plotting, mapping and so on. A good navigator could perhaps gain
additional range on a hyperspace jump, or succeed in identifying a location based on clues, etc.
Sensors
The sensors
attribute is intended to reflect the character’s ability to get the most from a ship’s scanner, radar, etc. This character might be able to find hidden ships, identify unknown cargo in space and so forth.
Using character attributes
These attributes are numeric, and are designed to be used in a similar way to
the atrtibutes on a table-top role-playing game character sheet. The
Character
class has methods for testing these attributes against
simulated dice rolls.
The DiceRoll()
method rolls four virtual sixteen-sided dice, and returns
the sum of their results. Here's a probability table.
TestRoll()
performs a dice roll using the method mentioned above, and
returns a boolean value; true means that the test passed, that the dice result
was numerically less than the attribute being tested. The higher the attribute,
the more likely it is to pass the test. The natural range of the dice is 4 to
64, with a most probable result of 34. The attribute's range can be much
greater than that. TestRoll()
can take an optional second argument,
which is a modifier; the modifier is added to the attribute for this roll only,
and the chance of any result changes accordingly.
If the result of the dice roll was very low or very high (below 9 or above 59) then it was a critical success, or a critical failure, respectively. The result is the same, but there is a side-effect that the character's attribute is permanently affected. A critical loss reduces the attribute by one, a critical success increases it by one. The chances of either happening are about 1%.
So, imagine the player wants to hire a crew member. The player has a long criminal record, so the potential crew member might not want to join the crew. To find out, we perform a test roll:
if PotentialCrewmember:TestRoll('lawfulness') then
UI.Message('Sorry, I just don't feel comfortable joining your crew')
-- Update the time and place, and release this character
PotentialCrewmember:Save()
else
UI.Message('I would love to join your crew!')
table.insert(crew,PotentialCrewMember)
end
If, for some reason, you really do not wish to modify the attribute, then there
is also a SafeRoll()
method, which acts almost exactly the same, except
that it has no critical success or failure system.
Arbitrary attributes
Any arbitrary attribute can be set in character, and tested using
TestRoll()
or SafeRoll()
. It's as simple as defining a new member
value. It can be done in the constructor, or afterwards:
c = Character.New({juggling=45})
c.sewing = c.DiceRoll() -- Randomize this attribute!
if not c:TestRoll('juggling') then
UI.Message(c.name .. " dropped a ball")
end
Perhaps there isn't much requirement for juggling or sewing on board a ship in Pioneer, but it does serve to illustrate that there are no limitations.
ChatForm faces
It's trivial to use a character on a ChatForm
in a BBS ad. The Character
class can be passed directly to the ChatForm.SetFace
method, placing the character's name, face and job title (if it is set) on the form.
ch = Character.New()
form:SetFace(ch)