Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save NathaTerrien/14536ac9eea2ca30023c to your computer and use it in GitHub Desktop.
Save NathaTerrien/14536ac9eea2ca30023c to your computer and use it in GitHub Desktop.
/*
Natha's Numenera rolls functions for Natha's Roll20 Numenera Character Sheet.
French
v2.7 - 2014/09/22
Documentation needed ...
*/
//-----------------------------------------------------------------------------
on("chat:message", function(msg) {
if (msg.type == "api") {
// sendChat("", "/desc TEST API.");
if(!msg.selected) {
sendChat("", "/desc Sélectionnez un pion et essayez encore.");
return; //quit if nothing selected
};
if (msg.content.indexOf("!nathanum-") !== 0) {
// sendChat("", "/desc Fonction inconnue.");
return;
} else {
var functionCalled = msg.content.split(" ")[0];
};
if (functionCalled == "!nathanum-numeneroll") {
var Parameters = msg.content.split("!nathanum-numeneroll ")[1];
var tokenId = Parameters.split("|")[0];
var tokenObj=getObj("graphic", tokenId);
var charId = tokenObj.get("represents");
if(charId != "") {
var characterObj = getObj("character", charId);
} else {
sendChat(""," Le pion sélectionné n'est pas un personnage.");
return false;
};
var statName = Parameters.split("|")[1];
var difficulty = parseInt(Parameters.split("|")[2]);
var effortsOnRoll = parseInt(Parameters.split("|")[3]);
var rollbonus = parseInt(Parameters.split("|")[4]);
var statexp = parseInt(Parameters.split("|")[5]);
var effortsOnDmg = parseInt(Parameters.split("|")[6]);
numeneRoll(characterObj,tokenObj,statName,difficulty,effortsOnRoll,rollbonus,statexp,msg.who,effortsOnDmg);
return;
};
if (functionCalled == "!nathanum-attrib") {
var Parameters = msg.content.split("!nathanum-attrib ")[1];
var tokenId = Parameters.split("|")[0];
var attributeName = Parameters.split("|")[1];
var newValue = Parameters.split("|")[2];
var tokenObj=getObj("graphic", tokenId);
var charId = tokenObj.get("represents");
if(charId != "") {
var characterObj = getObj("character", charId);
} else {
sendChat(""," Le pion sélectionné n'est pas un personnage.");
return false;
};
var attributeObjArray = findObjs({
_type: 'attribute',
name: attributeName,
_characterid: characterObj.id
});
if (attributeObjArray == false) return;
attrib(characterObj,attributeObjArray,newValue,tokenObj);
return;
};
if (functionCalled == "!nathanum-recoveryroll") {
var tokenId = msg.content.split("!nathanum-recoveryroll ")[1];
var tokenObj=getObj("graphic", tokenId);
var charId = tokenObj.get("represents");
if(charId != "") {
var characterObj = getObj("character", charId);
} else {
sendChat(""," Le pion sélectionné n'est pas un personnage.");
return false;
};
recoveryRoll(characterObj,tokenObj);
return;
};
if (functionCalled == "!nathanum-initroll") {
var Parameters = msg.content.split("!nathanum-initroll ")[1];
var tokenId = Parameters.split("|")[0];
var tokenObj=getObj("graphic", tokenId);
var charId = tokenObj.get("represents");
if(charId != "") {
var characterObj = getObj("character", charId);
} else {
sendChat(""," Le pion sélectionné n'est pas un personnage.");
return false;
};
var effortsUsed = parseInt(Parameters.split("|")[1]);
var rollbonus = parseInt(Parameters.split("|")[2]);
var statexp = parseInt(Parameters.split("|")[3]);
initRoll(characterObj,tokenObj,effortsUsed,rollbonus,statexp);
return;
};
if (functionCalled == "!nathanum-restchar") {
var Parameters = msg.content.split("!nathanum-restchar ")[1];
var tokenId = Parameters.split("|")[0];
var tokenObj=getObj("graphic", tokenId);
var charId = tokenObj.get("represents");
if(charId != "") {
var characterObj = getObj("character", charId);
} else {
sendChat(""," Le pion sélectionné n'est pas un personnage.");
return false;
};
restChar(characterObj,tokenObj);
return;
};
if (functionCalled == "!nathanum-npcdmg") {
var Parameters = msg.content.split("!nathanum-npcdmg ")[1];
var tokenId = Parameters.split("|")[0];
var dmgDealt = Parameters.split("|")[1];
var applyArmor = Parameters.split("|")[2];
var tokenObj=getObj("graphic", tokenId);
var charId = tokenObj.get("represents");
if(charId != "") {
var characterObj = getObj("character", charId);
} else {
sendChat("","The selected token doesn't represent a character.");
return false;
};
npcDamage(characterObj,tokenObj,dmgDealt,applyArmor);
return;
};
};
});
//-----------------------------------------------------------------------------
function attrib(characterObj,attributeObjArray,newValue,tokenObj) {
//modification d'un attribut d'un personnage
var attributeName = attributeObjArray[0].get("name");
var attributeValue = attributeObjArray[0].get("current");
var characterName = characterObj.get("name");
var maxValue = 0;
var finalValue = 0;
var attributeLib = "";
var checkState = 0;
// debug
// sendChat("", "/desc " + attributeName + " " + newValue + ".");
// change character attribute
switch (attributeName) {
case "might" :
maxValue = parseInt(attributeObjArray[0].get("max"));
finalValue = parseInt(attributeValue) + parseInt(newValue);
attributeLib = "Robustesse";
checkState = 1;
break;
case "speed" :
maxValue = parseInt(attributeObjArray[0].get("max"));
finalValue = parseInt(attributeValue) + parseInt(newValue);
attributeLib = "Vivacité";
checkState = 1;
break;
case "intellect" :
maxValue = parseInt(attributeObjArray[0].get("max"));
finalValue = parseInt(attributeValue) + parseInt(newValue);
attributeLib = "Mental";
checkState = 1;
break;
default:
finalValue = newValue;
maxValue = newValue;
attributeLib = attributeName;
}
if (finalValue > maxValue) {
finalValue = maxValue;
}
else {
if (finalValue < 0) {
finalValue = 0;
};
};
//set the attribute
attributeObjArray[0].set("current", finalValue);
//if attribute was one the stats, call the function to check state and markers
if (checkState == 1) {
checkCharStates(characterObj,tokenObj);
};
//output
sendChat("character|"+characterObj.get("id"), "<span style='color:blue;'>" + attributeLib + "("+attributeName+")" + "</span> : " + attributeValue + " -> " + finalValue + ".");
};
//-----------------------------------------------------------------------------
function checkCharStates(characterObj,tokenObj) {
// Recalcul de l'etat d'un personnage en fonction de ses attributs
//var tokenObj = getObj("graphic", tokenId); //DEBUG
var might = 0;
var speed = 0;
var intellect = 0;
var damage = 0;
var CurrentDamage = 0;
// reading all relevant stats and attributes from the character
might = parseInt(getAttrByName(characterObj.id, "might", "current"));
speed = parseInt(getAttrByName(characterObj.id, "speed", "current"));
intellect = parseInt(getAttrByName(characterObj.id, "intellect", "current"));
// calculating the damage track depending on the stats
// and setting the right markers depending on the damage track
if (might <= 0) {damage = damage +1};
if (speed <= 0) {damage = damage +1};
if (intellect <= 0) {damage = damage +1};
CurrentDamage = parseInt(getAttrByName(characterObj.id, "damage-track", "current"));
//sendChat("character|"+characterObj.get("id"), "Debug : Might = " + might + ", Damage Track = " + CurrentDamage + ", Damage = " + damage);
if (CurrentDamage != damage) {
var attDmgArray = findObjs({
_type: 'attribute',
name: "damage-track",
_characterid: characterObj.id
});
attDmgArray[0].set("current",damage);
switch (damage) {
case 0 : //normal state
tokenObj.set("status_yellow",false);
tokenObj.set("status_red",false);
tokenObj.set("status_dead",false);
break;
case 1 : // impaired
tokenObj.set("status_yellow",true);
tokenObj.set("status_red",false);
tokenObj.set("status_dead",false);
break;
case 2 : // disabled
tokenObj.set("status_yellow",true);
tokenObj.set("status_red",true);
tokenObj.set("status_dead",false);
break;
case 3 : // dead
tokenObj.set("status_yellow",false);
tokenObj.set("status_red",false);
tokenObj.set("status_dead",true);
break;
}
};
};
//-----------------------------------------------------------------------------
function restChar(characterObj,tokenObj) {
//repos total d'un personnage
//sendChat("", "/desc Repos"); //DEBUG
var characterName = characterObj.get("name");
// MIGHT
var attObjArray = findObjs({
_type: 'attribute',
name: "might",
_characterid: characterObj.id
});
attObjArray[0].set("current", attObjArray[0].get("max"));
// SPEED, taking account of the armor speed reduction
attObjArray = findObjs({
_type: 'attribute',
name: "speed",
_characterid: characterObj.id
});
var maxspeed = parseInt(attObjArray[0].get("max"));
var speedreduction = parseInt(getAttrByName(characterObj.id, "armor-speed-reduction", "current"));
attObjArray[0].set("current", maxspeed - speedreduction);
// INTELLECT
attObjArray = findObjs({
_type: 'attribute',
name: "intellect",
_characterid: characterObj.id
});
attObjArray[0].set("current", attObjArray[0].get("max"));
//Recovery rolls
attObjArray = findObjs({
_type: 'attribute',
name: "recovery-rolls",
_characterid: characterObj.id
});
attObjArray[0].set("current", "0");
//Markers & States & Damage track
checkCharStates(characterObj,tokenObj);
//output
sendChat("character|"+characterObj.get("id"), "<span style='color:green;'>est complètement reposé.</span>");
};
//-----------------------------------------------------------------------------
function initRoll(characterObj,tokenObj,efforts,rollBonus,statexp) {
// BEWARE : Not the standard Numenera initiative roll !
// Add the effort and rollbonus to the roll and add it to the tracker.
// Meant to be sorted/compared to Level*3 of the NPCs/Creatures
var charId = characterObj.get("id");
var speedPool = 0;
var speedEdge = 0;
var attrEffort = 0;
//getting the stats values
var attributeObjArray = findObjs({
_type: 'attribute',
name: 'speed',
_characterid: characterObj.id
});
if (attributeObjArray == false) {
sendChat("", "/desc ERREUR : iniRoll() : attribute 'speed' inconnu.");
return;
} else {
speedPool=parseInt(attributeObjArray[0].get("current"));
};
speedEdge = parseInt(getAttrByName(characterObj.id, "speededge", "current"));
attrEffort = parseInt(getAttrByName(characterObj.id, "effort", "current"));
var damagetrack=parseInt(getAttrByName(characterObj.id, "damage-track", "current"));
var bonusToRoll = parseInt(rollBonus);
// Rolling the dice
var diceRoll = randomInteger(20);
// checking for appliable effort
// and calculating statpool cost
var effortsUsed = 0;
effortsUsed = parseInt(efforts);
var statExpense = parseInt(statexp);
var effortCost = 0;
var totalCost = 0;
if ((effortsUsed > 0) || (statExpense > 0)) {
if (effortsUsed > 0) {
if (effortsUsed > attrEffort) {
sendChat("character|"+charId, "<span style='color:red;'>essaie d'appliquer plus d'effort ("+effortsUsed+") qu'il ne peut ("+attrEffort+").</span>");
return;
} else {
effortCost = 1 + (effortsUsed * (2+damagetrack) );
};
};
// effort cost is spent if the roll is not 20 and if the action succeeds
if (diceRoll != 20) {
totalCost = effortCost + statExpense - speedEdge;
if (totalCost > speedPool) {
sendChat("character|"+charId, "<span style='color:red;'>essaie de dépenser plus de points de Vivacité ("+totalCost+") qu'il ne peut ("+speedPool+").</span>");
return;
} else {
speedPool = speedPool - totalCost;
attributeObjArray[0].set("current",speedPool);
checkCharStates(characterObj,tokenObj);
};
};
};
// Calclating initiative
var finalRoll = diceRoll + (effortsUsed*3) + bonusToRoll;
var turnorder;
if(Campaign().get("turnorder") == "") {
turnorder = [];
} else turnorder = JSON.parse(Campaign().get("turnorder"));
turnorder.push({
id: tokenObj.id,
pr: finalRoll,
});
turnorder.sort(function(a,b) {
first = a.pr;
second = b.pr;
return second - first;
});
Campaign().set("turnorder", JSON.stringify(turnorder));
//output
// Checking result
var success = "<b><span style='color:blue;'>Jet d'Initiative : " + finalRoll + "</span> (" + diceRoll + ")</b><ul style='list-style-type : disc;'>";
if ((effortsUsed > 0) || (statExpense > 0)) {
success = success + "<li>Dépense " + totalCost + " point(s) de Vivacité (" + effortCost + "+" + statExpense + "-" + speedEdge + ").</li>";
};
if (effortsUsed > 0) {
success = success + "<li>En appliquant " + effortsUsed + " <u>Effort(s)</u>.</li>";
};
if (bonusToRoll != 0) {
success = success + "<li>Avec un <u>Bonus</u> de " + bonusToRoll + ".</li>";
};
sendChat("character|"+charId, ""+success+"</ul>");
};
//-----------------------------------------------------------------------------
function numeneRoll(characterObj,tokenObj,statName,difficulty,effortsOnRoll,rollBonus,statexp,whoRolled,effortsOnDmg) {
// ROll D20 with eventual might, speed or intellect effort(s), versus a difficulty (optional)
// use "!numeneroll" in the chat or macro with parameters like these, separated with pipes : name-of-the-stat|efforts-used|difficulty
// name-of-the-stat might be one of : might or speed or intellect
// example : "!numeneroll might|1|3"
// difficulty is the level of difficulty, not the target number. The target number is calculated by the function.
// if difficulty is 0, the roll still happens, but is not confronted to any difficulty
var charId = characterObj.get("id");
//checking the character damage track
var damagetrack=parseInt(getAttrByName(characterObj.id, "damage-track", "current"));
if (damagetrack > 1) {
sendChat("character|"+charId, "<span style='color:red;'>ne peut pas entreprendre d'action (épuisé ou mourrant).</span>");
return;
};
// checking the stat
var attributeName = "";
var edgename = "";
if(statName == "might" || statName == "speed" || statName == "intellect") {
attributeName = statName;
edgename = statName + "edge";
// Stat text to show in chat
var attNameInChat = "";
switch (statName) {
case "might" :
attNameInChat = "Robustesse";
break;
case "speed" :
attNameInChat = "Vivacité";
break;
case "intellect" :
attNameInChat = "Mental";
break;
}
} else {
sendChat("", "/desc ERREUR : numeneRoll() : " + statName + " n'est pas géré.");
return;
};
//getting the stats values
var attrPool = 0;
var attrEdge = 0;
var attrEffort = 0;
var attributeObjArray = findObjs({
_type: 'attribute',
name: attributeName,
_characterid: characterObj.id
});
if (attributeObjArray == false) {
sendChat("", "/desc ERREUR : numeneRoll() : attribute '" + attributeName + "' inconnu.");
return;
} else {
attrPool=parseInt(attributeObjArray[0].get("current"));
};
attrEdge = parseInt(getAttrByName(characterObj.id, edgename, "current"));
attrEffort = parseInt(getAttrByName(characterObj.id, "effort", "current"));
//Checking the bonus to roll
var bonusToRoll = parseInt(rollBonus);
if( bonusToRoll >= 3 ) {
sendChat("character|"+charId, "<span style='color:red;'>un bonus supérieur à 2 ("+bonusToRoll+") n'est pas autorisé (utilisez un Avantage)</span>.");
return;
};
// Rolling the dice
var diceRoll = randomInteger(20);
var finalRoll = diceRoll + bonusToRoll;
// checking for appliable effort
// and calculating statpool cost
var effortsUsed = 0;
var effortRoll = parseInt(0 || effortsOnRoll);
var effortDmg = parseInt(0 || effortsOnDmg),
effortsUsed = effortRoll + effortDmg;
var statExpense = parseInt(statexp);
var effortCost = 0;
var totalCost = 0;
if ((effortsUsed > 0) || (statExpense > 0)) {
if (effortsUsed > 0) {
if (effortsUsed > attrEffort) {
sendChat("character|"+charId, "<span style='color:red;'>essaie d'appliquer plus d'effort ("+effortsUsed+") qu'il ne peut ("+attrEffort+").</span>");
return;
} else {
effortCost = 1 + (effortsUsed * (2+damagetrack) );
};
};
// effort cost is spent if the roll is not 20 and if the action succeeds
if (diceRoll != 20) {
totalCost = effortCost + statExpense - attrEdge;
if (totalCost > attrPool) {
sendChat("character|"+charId, "<span style='color:red;'>essaie de dépenser plus de points d'attribut ("+totalCost+") qu'il ne peut ("+attrPool+").</span>");
return;
} else {
attrPool = attrPool - totalCost;
attributeObjArray[0].set("current",attrPool);
checkCharStates(characterObj,tokenObj);
};
};
};
//computing target task
var targetRoll = 0;
if (parseInt(difficulty) > 0) {
targetRoll = (parseInt(difficulty)-effortRoll)*3;
};
// Checking result
var success = "<b><span style='color:blue;'>Jet de " + attNameInChat + "</span></b> (Atout :"+attrEdge+")<ul style='list-style-type : disc;'>";
var specialEffect = "";
if ((effortsUsed > 0) || (statExpense > 0)) {
success = success + "<li><u>Dépense</u> " + totalCost + " point(s) (" + effortCost + "+" + statExpense + "-" + attrEdge + ") :<ul>";
if (effortRoll > 0) {
success = success + "<li>Effort(s) au <u>jet</u> : " + effortRoll + "</li>";
};
if (effortDmg > 0) {
success = success + "<li>Effort(s) aux <u>dégâts</u> : " + effortDmg + " (+"+(effortDmg*3)+" points de dégâts)</li>";
};
if (statExpense > 0) {
success = success + "<li>Coût supplémentaire : " + statExpense + "</li>";
};
success = success + "</ul></li>";
};
if (bonusToRoll != 0) {
success = success + "<li>Avec un <u>Bonus</u> de " + bonusToRoll + ".</li>";
};
// If not an automatic success or a known difficulty
if (targetRoll > 0) {
success = success + "<li>Contre une <u>Difficulté</u> initiale de " + difficulty + " (cible : " + targetRoll + ").</li>";
if( finalRoll >= targetRoll) {
success = success + "<li><strong><span style='color:green;'>Réussite (" + diceRoll+"+"+bonusToRoll+" = "+finalRoll + ") !</span></strong></li>";
}
else{
success = success + "<li><strong><span style='color:red;'>Echec (" + diceRoll+"+"+bonusToRoll+" = "+finalRoll + ") !</span></strong></li>";
};
//special dice roll result treatment
if (diceRoll == 1) {
specialEffect = "<li><u>Effet supplémentaire</u> : <span style='color:red;'>subit dégâts +2 (en défense) ou une intrusion du MJ !</span></li>";
} else {
if (damagetrack == 0) {
// if character is haled, special success is possible
switch (diceRoll) {
case 17:
specialEffect = "<li><u>Effet supplémentaire</u> : <span style='color:green;'>si jet d'attaque : Dégâts +1.</span></li>";
break;
case 18:
specialEffect = "<li><u>Effet supplémentaire</u> : <span style='color:green;'>si jet d'attaque : Dégâts +2.</span></li>";
break;
case 19:
specialEffect = "<li><u>Effet supplémentaire</u> : <span style='color:green;'>si jet d'attaque : Dégâts +3, ou un Effet Mineur.</span></li>";
break;
case 20:
specialEffect = "<li><u>Effets supplémentaires</u> : <span style='color:green;'>si jet d'attaque : Dégâts +4, ou un Effet Majeur.<br/>Points d'Attribut non dépensés.</span></li>";
break;
default:
specialEffect = "";
}
} else if (diceRoll == 20) {
// if character is impaired
specialEffect = "<li><u>Effet supplémentaire</u> : <span style='color:green;'>Points d'Attribut non dépensés.</span></li>";
};
};
} else { // automatic success or no known difficulty
if (parseInt(difficulty) > 0) {
success = success + "<li><strong><span style='color:green;'>Et réussit automatiquement (Difficulté " + difficulty + " / cible : " + targetRoll + ") !</span></strong></li>";
} else {
success = success + "<li><u>Résultat</u> : <strong>" + finalRoll + ".</strong></li>";
};
};
//output
sendChat("character|"+charId, ""+success+specialEffect+"</ul>");
};
//-----------------------------------------------------------------------------
function recoveryRoll(characterObj,tokenObj){
var charId = characterObj.get("id");
var recovObjArray = findObjs({
_type: 'attribute',
name: "recovery-rolls",
_characterid: charId
});
if (recovObjArray == false) {
sendChat("", "ERREUR : pas d'attribut 'recovery-rolls' !");
return;
};
var recrolls=parseInt(recovObjArray[0].get("current"));
//Is a complete rest of 10 hours needed ?
if (recrolls == 3) {
sendChat("character|"+charId, "<span style='color:red;'>ne peut pas faire de jet de récupération (doit se reposer complètement pendant 10 heures).</span>");
return;
};
var recovbonus = parseInt(getAttrByName(charId, "recoverybonus", "current"));
var curRecLib = "";
var nextRecLib = "";
switch (recrolls) {
case 0 :
curRecLib = "1 action";
nextRecLib = "10 minutes";
break;
case 1 :
curRecLib = "10 minutes";
nextRecLib = "1 heure";
break;
case 2 :
curRecLib = "1 heure";
nextRecLib = "Repos complet (10 heures)";
break;
}
recrolls=recrolls+1;
recovObjArray[0].set("current",recrolls);
var recovery = randomInteger(6)+recovbonus;
var output = "<b><span style='color:blue;'>Jet de récupération</span></b> : <ul style='list-style-type : disc;'>";
output = output + "<li>Repos de <b>" + curRecLib + "</b>.</li>"
output = output + "<li><b><span style='color:green;'>" + recovery + " points</span></b> à répartir dans les attributs.</li>"
output = output + "<li>Prochain jet/repos : <b>" + nextRecLib + "</b>.</li>";
sendChat("character|" + charId, "" + output +"</ul>");
};
//------------------------------------------------------------------------------
function npcDamage(characterObj,tokenObj,dmgDealt, applyArmor) {
// Apply damage (or healing if dmdDeal is negative ...) to Numenera NPC/Creature
// And set "death" marker if health is 0 or less.
// The Mook or Non Player full Character must have the following attributes :
// - Level (token bar1)
// - Health (token bar2)
// - Armor (token bar3)
// Armor will diminish damage unless "applyArmor"='n'
var dmg = parseInt(dmgDealt);
if (isNaN(dmg)) {dmg=0;};
var npcName = characterObj.get("name");
var npcHealth = 0;
var npcMaxHealth = 0;
if (applyArmor=="n"){
var npcArmor = 0;
} else {
var npcArmor = parseInt(getAttrByName(characterObj.id, "Armor", "current"));
if (isNaN(npcArmor)) {npcArmor=0;};
};
// Is the token linked to the character ("full NPC") or a Mook ?
var isChar = tokenObj.get("bar1_link");
if (isChar == "") {
// It's a Mook : get the bars value
npcHealth = parseInt(tokenObj.get("bar2_value"));
npcMaxHealth = parseInt(tokenObj.get("bar2_max"));
} else {
// It's a "full" character NPC : get the attributes values
var attObjArray = findObjs({
_type: 'attribute',
name: "Health",
_characterid: characterObj.id
});
if (attObjArray == false) {
sendChat("GM", "/w gm npcDamage() Error : this character has no Health attribute!");
return;
} else {
npcHealth=parseInt(attObjArray[0].get("current"));
npcMaxHealth=parseInt(attObjArray[0].get("max"));
};
};
if (isNaN(npcHealth)) {npcHealth=0;};
if (isNaN(npcMaxHealth)) {npcMaxHealth=0;};
// In case the Health attribute / bar has no maximum value
npcMaxHealth = Math.max(npcHealth, npcMaxHealth);
if (dmg > 0) {
dmg = Math.max((dmg - npcArmor),0);
};
var npcHealthFinal = Math.min(Math.max((npcHealth - dmg),0),npcMaxHealth);
if (isChar == "") {
// Mook : update bars onbly
tokenObj.set("bar2_max", npcMaxHealth);
tokenObj.set("bar2_value",npcHealthFinal);
} else {
// Update character attributes
attObjArray[0].set("max",npcMaxHealth);
attObjArray[0].set("current",npcHealthFinal);
};
if (npcHealthFinal == 0){
tokenObj.set("status_dead", true);
} else {
tokenObj.set("status_dead", false);
};
sendChat("GM", "/w gm " + npcName + " takes " + dmg + "/" + dmgDealt + " damage. Health: " + npcHealth + "->" + npcHealthFinal + ".");
return;
};
//------------------------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment