RPG Library ‐ NPCs and Monsters - ThePix/QuestJS GitHub Wiki
You need to use a special template for the player object and all your NPCs.
Go here for the introduction to RPG, or here for how to spawn more monsters during play. See here for a list of attributes.
RPG_PLAYER
This should be used for the player rather than the normal PLAYER template. It combines PLAYER and RPG_TEMPLATE.
createItem("me", RPG_PLAYER(), {
loc:"practice_room",
regex:/^(me|myself|player)$/,
health:100,
offensiveBonus_weapon:3,
offensiveBonus_spell:2,
examine:"Just a regular spell-slinging guy.",
})
RPG_NPC
This, or one of the several templates that are derived from it, should be used for all NPCs - including any monsters - rather than the normal NPC template; you will get a warning in the console if it is not. It combines NPC and RPG_TEMPLATE, and can be sent a Boolean, true
if female, false
if male (but as with NPC you can override that).
Here is a very simple example. Note that it sets the NPC's health and the damage it does.
createItem("goblin", RPG_NPC(false), {
loc:"practice_room",
damage:"d8",
health:40,
offensiveBonus:1,
ex:"A rather small green humanoid; hairless and dressed in rags.",
})
The RPG module uses "ex" rather than "examine" for NPC descriptions; this allows it to add more information relating to the state of the NPC. For example, if settings.includeHitsInExamine
is true
, the user will see the current hits and maximum hits for the NPC.
Note that if this is a function, it must return a string, rather than display it itself (like location descriptions). It can also be string as normal.
If the NPC is dead or asleep, Quest will append a comment. Alternatively you can give an NPC an "exDead" or "exAsleep" description.
There are specialised templates described here.
Attacking
There are broadly three approaches to giving your NPC an attack. The simplest uses the NPC itself as the weapon; just give the NPC a damage attribute, as you see for the goblin above.
If the NPC has a weapon, a better approach is to create the weapon and assign it. This means that when the NPC is killed, the weapon can be picked up. This example gives us an orc with both a sword and shield.
createItem("orc", RPG_NPC(false), {
loc:"practice_room",
damage:"2d6",
health:60,
ex:"A large green humanoid; hairless and dressed in leather.",
weapon:'orc_sword',
shield:"huge_shield",
})
createItem("orc_sword", WEAPON('2d10+4'), {
examine:'A crude, but hefty sword.',
})
createItem("huge_shield", SHIELD(10), {
examine:'A very big shield.',
})
Note that the location of the sword and shield will get automatically set to the orc to ensure they are there to be picked up when it is killed. You just need to set the "weapon" and "shield" attributes of the orc to ensure they are equipped. If the name ends "_prototype", the NPC will get a clone of the item, rather than the item itself.
You could change the attributes during the game; perhaps the player can catch the orc unawares while it is unarmed, or has a skill to disarm it. To allow for this, the orc also has a "damage" attribute, which will be used when no weapon is set. It is up to you to ensure the "loc" attribute of the weapon or shield are set appropriately - Quest only does that at the start.
The third approach to NPC attacks is the multi-attack...
Multi-attack
By default, NPCs have a single attack. Often you will want an NPC to have a number of different attacks. The goblin shaman, for example, can cast any spell from her spell book.
To handle that, give the NPC a "skillOptions" attribute, a list of spells and skills she can use. One will be selected at random for each attack.
createItem("goblin_shaman", RPG_NPC(true), {
loc:"practice_room",
health:40,
defensiveBonus:2,
skillOptions:['Ice shard', 'Fireball'],
ex:"A rather small green humanoid; hairless and dressed in rags and bizarre bone-jewellery.",
})
This example uses the built-in spells, but in my opinion creating specialised spells adds more flavour. You are not limited to spells; your dragon might have a tail slam attack, a claw attack, a bite attack and fire breathe. Create each as a skill (I suggest prefixing the name of each "dragon_"), and list them all on the "skillOptions" attribute.
Here is a contrived example from the unit tests. Four attacks are created (that do not actually do anything!), the third being preparation for the fourth. All but the last are give in the "skillOptions" attribute. If the third is selected, the player is informed the orc is preparing a special attack, giving her a chance to take some defensive action. The next turn, as the orc's "nextAttack" attribute is set, the list of options is ignored and instead the fourth attack is used.
new Spell("Test attack 1", {
primarySuccess:"Test attack one was performed",
})
new Spell("Test attack 2", {
primarySuccess:"Test attack two was performed",
})
new Spell("Test attack 3A", {
primarySuccess:"Test attack three was prepared",
afterUse:function(attack) {
attack.attacker.nextAttack = "Test attack 3B"
}
})
new Spell("Test attack 3B", {
primarySuccess:"Test attack three was performed",
})
w.orc.skillOptions = ['Test attack 1', 'Test attack 2', "Test attack 3A"]
You can change the "skillOptions" attribute as the game progresses, giving an NPC new options as time passes, or conversely removing options as the player progressively weakens it. Perhaps the first attack is always one skill/spell, but after that it has a choice of several.
You can also give an NPC a custom "selectSkill" attribute. This should return the skill/spell to use, and you can have it do anything you want. You should take account of the character's state - can she currently cast spells, etc. To see how this might be done, this is the default script:
res.selectSkill = function(target) {
const weapon = this.getEquippedWeapon()
if (!this.skillOptions) {
if (this.attackProhibited) return null
return weapon ? defaultWeaponAttack : defaultNaturalAttack
}
let skills = this.skillOptions.map(el => rpg.findSkill(el))
if (!this.testActivity('castSpell')) skills = skills.filter(el => !el.type === 'spell')
if (!this.testActivity('attack')) skills = skills.filter(el => el.type === 'spell')
if (!this.testActivity('breath')) skills = skills.filter(el => el.type !== 'breath')
if (!weapon) skills = skills.filter(el => el.noWeapon)
// do we also need to check if a weapon is available?
if (skills.length === 0) return null
const skillName = random.fromArray(this.skillOptions)
return rpg.findSkill(skillName)
}
Looting the Dead
An important part of any RPG seems to be looting the dead. The RPG library adds a SEARCH command to handle that. If the NPC is alive and awake, the lang.searchAlive
response is given and the command fails. If the NPC has a "searchWhenDead" attribute, the command will use that, otherwise, if there is default action is set in settings.defaultSearch
it will use that, otherwise it will just report that nothing is found. Repeatedly searching the same NPC will just give a message saying nothing more is found.
There are, therefore, two approaches you can take to handling searching, and you can use both in the same game. You can set settings.defaultSearch
function to handle it across all your NPCs, and you can also override the "searchWhenDead" function attribute on a specific NPC.
You could set up the "search" attribute like this. This simple example just adds 9 to the player's money, and then tells the player.
searchWhenDead:function(options) {
player.money += 9
msg("You find 9 gold coins on the body.")
},
This version allows for NPCs to search bodies too.
searchWhenDead:function(options) {
options.char.money += 9
msg("{nv:char:find:true} 9 gold coins on the body.", options)
},
Your settings.defaultSearch
might look like this. I am adding the amount of gold found to the options
so it can be accessed by the text processor, and making the amount at least 2 so I do not have to worry about coin/coins.
settings.defaultSearch = function(npc, options) {
options.gp = random.int(2,10)
options.char.money += options.gp
log(options)
msg("{nv:char:find:true} {number:gp} gold coins on the body.", options)
}
You could have this vary depending on the "level" or "maxHealth" of the NPC, have a small chance of a magic item, or any amount of other clutter.
Note that you can check the "searched" attribute of an NPC to see if he has already been searched.
Situation flags
To allow spells, etc. to affect a character in various ways, the system has various situation flags, such as stunned and blinded. These are all undefined
or false
by default on a character.
These situation flags are used by effects, and you might wonder why not just use effects? The advantage of doing it this way is that the library does not need to know all the effects; it can decide if the character can cast a spell based on the state flags, while the author can invent any new effects she likes and both just need to interact with the flags and not worry about the other. It also allows for a character to be affected by multiple effects. Note however that you should not set these except through the "flags" attributes of an effect, as they are likely to be turned off at the end of each turn.
The built-in states are (in the "nullified" state, no spell can be cast):
'dead', 'blinded', 'stunned', 'petrified', 'asleep', 'mute', 'befuddled', 'immobilised', 'paralysed', 'unhanded', 'nullified'
You can add your own simply by appending to rpg.situations
in settings.setup
. And by the way, some games really do use "wet" as a status effect!
rpg.situations.push('wet')
There is also a list of activities that determine what the character can do when in a certain state ("breath" is using a breath attack, like a dragon).
'posture', 'move', 'manipulate', 'talk', 'castSpell', 'castSpellSelf', 'castSpellMind', 'weapon', 'natural', 'breath'
Again, you can add your own, but it is a bit more complicated as you need to add a dictionary, not just a name. The dictionary needs to flag each situation where the situation is allowed.
rpg.activities.push({name:'remember', allow_immobilised:true, allow_dumb:true, allow_unhanded:true})
Going back to our "wet" situation, we will need to update the existing activities. We might decide you can still move to another location when wet.
rpg.activities.find(el => el.name === 'move').allow_wet = true
Actually, you might decide you can do all the activities when wet except one.
for (const act of rpg.activities) act.allow_wet = true
rpg.activities.find(el => el.name === 'breath').allow_wet = undefined