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)