Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NPC Role selection #173

Merged
merged 6 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"ExpAbbr": "XP",
"Level": "Level",
"LevelAbbr": "LV",
"Role": "Role",
"LevelUp": "LEVEL UP!",
"Name": "Name",
"Roll": "Roll",
Expand Down Expand Up @@ -384,6 +385,7 @@
"RangedAccuracyBonus": "Ranged Accuracy Bonus",
"MeleeDamageBonus": "Melee Damage Bonus",
"RangedDamageBonus": "Ranged Damage Bonus",
"SpellDamageBonus": "Spell Damage Bonus",
"HealthBonusSL": "HP Bonus x SL",
"MindBonusSL": "MP Bonus x SL",
"InventoryBonusSL": "IP Bonus x SL",
Expand Down Expand Up @@ -1215,6 +1217,12 @@
"ChangeWarning2": "Changing an Item's Fabula Ultima ID might break references to the Item! Proceed with caution and only if you are sure.",
"ChangeWarning3": "Do you want to proceed?",
"ChangeWarning1": "Proceed with caution!"
}
},
"Brute": "Brute",
"Hunter": "Hunter",
"Mage": "Mage",
"Saboteur": "Saboteur",
"Sentinel": "Sentinel",
"Support": "Support"
}
}
2 changes: 1 addition & 1 deletion module/documents/actors/common/attribute-data-model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class AttributeDataModel extends foundry.abstract.DataModel {
super(data, options);
let current = this.base;
Object.defineProperty(this, 'current', {
configurable: false,
configurable: true,
enumerable: true,
get: () => {
return MathHelper.clamp(2 * Math.floor(current / 2), 6, 12);
Expand Down
58 changes: 57 additions & 1 deletion module/documents/actors/npc/npc-data-model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { ImmunitiesDataModel } from '../common/immunities-data-model.mjs';
import { NpcSkillTracker } from './npc-skill-tracker.mjs';
import { EquipDataModel } from '../common/equip-data-model.mjs';
import { DerivedValuesDataModel } from '../common/derived-values-data-model.mjs';
import { Role } from '../../../helpers/roles.mjs';

Hooks.on('preUpdateActor', (document, changed) => {
Hooks.on('preUpdateActor', async (document, changed) => {
if (document.system instanceof NpcDataModel) {
const newVillainType = foundry.utils.getProperty(changed, 'system.villain.value');
if (newVillainType !== undefined && newVillainType !== document.system.villain.value) {
Expand All @@ -20,6 +21,15 @@ Hooks.on('preUpdateActor', (document, changed) => {

foundry.utils.setProperty(changed, 'system.resources.fp.value', ultimaPoints[newVillainType] ?? 0);
}

// If role or level changed
const newRole = foundry.utils.getProperty(changed, 'system.role.value');
let roleChanged = newRole !== undefined && newRole !== document.system.role.value;
const newLevel = foundry.utils.getProperty(changed, 'system.level.value');
let levelChanged = newLevel !== undefined && newLevel !== document.system.level.value;
if (roleChanged || levelChanged) {
setRoleAttributes(document, newRole, newLevel);
}
}
});

Expand Down Expand Up @@ -58,6 +68,7 @@ Hooks.on('preUpdateActor', (document, changed) => {
* @property {number} phases.value
* @property {string} multipart.value
* @property {"soldier", "elite", "champion", "companion"} rank.value
* @property {RoleType} role.value
* @property {number} rank.replacedSoldiers
* @property {number} companion.playerLevel
* @property {number} companion.skillLevel
Expand Down Expand Up @@ -99,6 +110,9 @@ export class NpcDataModel extends foundry.abstract.TypeDataModel {
value: new StringField({ initial: 'soldier', choices: Object.keys(FU.rank) }),
replacedSoldiers: new NumberField({ initial: 1, min: 0, max: 6 }),
}),
role: new SchemaField({
value: new StringField({ initial: 'custom', choices: Object.keys(FU.role) }),
}),
companion: new SchemaField({
playerLevel: new NumberField({ initial: 1, min: 1, integer: true, nullable: false }),
skillLevel: new NumberField({ initial: 1, min: 1, integer: true, nullable: false }),
Expand Down Expand Up @@ -186,3 +200,45 @@ export class NpcDataModel extends foundry.abstract.TypeDataModel {
});
}
}

/**
* Sets the NPC's attributes and bonuses based on its role if set
* @param {FUStandardActorSheet} actor
* @param {*} roleType
* @returns
*/
async function setRoleAttributes(actor, newRole, newLevel) {
const role = newRole ?? actor.system.role.value;

// Do nothing if the role was set to custom
if (role == 'custom') {
return;
}

const level = newLevel ?? actor.system.level.value;
console.info(`Setting attributes for role ${role} at level ${level}`);
const updates = {};

// Set accuracy/damage bonuses
let accuracyBonus = Math.floor(level / 10);
updates['system.bonuses.accuracy.accuracyCheck'] = accuracyBonus;
updates['system.bonuses.accuracy.magicCheck'] = accuracyBonus;
let damageBonus = Math.floor(level / 20) * 5;
updates['system.bonuses.damage.melee'] = damageBonus;
updates['system.bonuses.damage.ranged'] = damageBonus;
updates['system.bonuses.damage.spell'] = damageBonus;

// Set attributes
let roleData = Role.resolve(role);
let attributes = roleData.getAttributesForLevel(level);
updates['system.attributes.dex.base'] = attributes.dex;
updates['system.attributes.ins.base'] = attributes.ins;
updates['system.attributes.mig.base'] = attributes.mig;
updates['system.attributes.wlp.base'] = attributes.wlp;

// TODO: Restore HP/MP to maximum

if (Object.keys(updates).length > 0) {
actor.update(updates);
}
}
17 changes: 17 additions & 0 deletions module/helpers/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,20 @@ FU.rank = {
champion: 'FU.Champion',
companion: 'FU.Companion',
};

/**
* @typedef {"custom", "brute", "hunter", "mage", "saboteur", "sentinel", "support"} RoleType
*/

/**
* @type {Object<RoleType, string>}
*/
FU.role = {
custom: 'FU.Custom',
brute: 'FU.Brute',
hunter: 'FU.Hunter',
mage: 'FU.Mage',
saboteur: 'FU.Saboteur',
sentinel: 'FU.Sentinel',
support: 'FU.Support',
};
145 changes: 145 additions & 0 deletions module/helpers/roles.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* The following abstractions can be remodeled or folded into
* a general one adopted by the system at a later date;
* at the time of writing this was the simplest approach I could make.
*/

/**
* An array of the 4 attributes used by a character in the system.
*/
export class AttributeArray {
/**
* Constructs an array of the 4 attributes used by a character in the system
* @param {Number} dex
* @param {Number} ins
* @param {Number} mig
* @param {Number} wlp
*/
constructor(dex, ins, mig, wlp) {
this._dex = dex;
this._ins = ins;
this._mig = mig;
this._wlp = wlp;
}

get dex() {
return this._dex;
}

get mig() {
return this._mig;
}

get ins() {
return this._ins;
}

get wlp() {
return this._wlp;
}
}

/**
* Model for an NPC role, which determines its attributes, skills, etc.
*/
export class Role {
/**
* @param {*} attributeSteps The attributes for each step in advancement
*/
constructor(attributeSteps) {
this.attributeSteps = attributeSteps;
}

/**
* @param {Number} level
* @returns {AttributeArray} The atttibutes for that level
*/
getAttributesForLevel(level) {
// There's steps at levels: BASE, 20, 40, 60
let step = Math.floor(level / 20);
console.info(`Getting attributes for level ${level}, step ${step}`);
return this.attributeSteps[step];
}

/**
* @param {RoleType} roleType
* @returns {Role}
*/
static resolve(roleType) {
switch (roleType) {
case 'brute':
return this.brute;
case 'hunter':
return this.hunter;
case 'mage':
return this.mage;
case 'saboteur':
return this.saboteur;
case 'sentinel':
return this.sentinel;
case 'support':
return this.support;
}
return null;
}

static get brute() {
let attributeSteps = {};
attributeSteps[0] = new AttributeArray(8, 6, 10, 8);
attributeSteps[1] = new AttributeArray(8, 8, 10, 8);
attributeSteps[2] = new AttributeArray(8, 6, 10, 10);
attributeSteps[3] = new AttributeArray(8, 6, 12, 10);
let role = new Role(attributeSteps);
return role;
}

static get hunter() {
let attributeSteps = {};
attributeSteps[0] = new AttributeArray(10, 8, 8, 6);
attributeSteps[1] = new AttributeArray(10, 8, 8, 8);
attributeSteps[2] = new AttributeArray(12, 8, 8, 8);
attributeSteps[3] = new AttributeArray(12, 10, 8, 8);
let role = new Role(attributeSteps);
return role;
}

static get mage() {
let attributeSteps = {};
attributeSteps[0] = new AttributeArray(8, 8, 6, 10);
attributeSteps[1] = new AttributeArray(8, 10, 6, 10);
attributeSteps[2] = new AttributeArray(8, 10, 6, 12);
attributeSteps[3] = new AttributeArray(8, 10, 8, 12);
let role = new Role(attributeSteps);
return role;
}

static get saboteur() {
let attributeSteps = {};
attributeSteps[0] = new AttributeArray(10, 8, 6, 8);
attributeSteps[1] = new AttributeArray(10, 8, 8, 8);
attributeSteps[2] = new AttributeArray(10, 10, 8, 8);
attributeSteps[3] = new AttributeArray(12, 10, 8, 8);
let role = new Role(attributeSteps);
return role;
}

static get sentinel() {
let attributeSteps = {};
attributeSteps[0] = new AttributeArray(8, 8, 8, 8);
attributeSteps[1] = new AttributeArray(8, 8, 10, 8);
attributeSteps[2] = new AttributeArray(8, 8, 10, 10);
attributeSteps[3] = new AttributeArray(10, 8, 10, 10);
let role = new Role(attributeSteps);
return role;
}

static get support() {
let attributeSteps = {};
attributeSteps[0] = new AttributeArray(8, 8, 6, 10);
attributeSteps[1] = new AttributeArray(8, 10, 6, 10);
attributeSteps[2] = new AttributeArray(8, 10, 8, 10);
attributeSteps[3] = new AttributeArray(8, 12, 8, 10);
let role = new Role(attributeSteps);
return role;
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions templates/actor/partials/actor-charname.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,23 @@
<select name="system.rank.value" class="resource-inputs rank-select">
{{selectOptions FU.rank selected=system.rank.value localize=true}}
</select>

{{#if (eq system.rank.value 'champion')}}
<span class="resource-inputs flex0 champion-input">
<input type="number" name="system.rank.replacedSoldiers" min="1" max="6" value="{{system.rank.replacedSoldiers}}"
class="resource-inputs" />
</span>
{{/if}}
</div>
<!-- Role -->
<label class="resource-label-m align-center backgroundstyle" data-tooltip="{{localize 'FU.Role'}}" aria-describedby="tooltip">
<i class="fas fa-wrench-simple icon"></i>
{{localize 'FU.Role'}}
</label>
<div class="grid-span-2 flexrow">
<select name="system.role.value" class="resource-inputs">
{{selectOptions FU.role selected=system.role.value localize=true}}
</select>
</div>
</div>
{{/if}}
25 changes: 25 additions & 0 deletions templates/actor/sections/actor-section-settings.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,31 @@
includeZero=true}}disabled
data-tooltip="{{localize "FU.DisabledByActiveEffect"}}"{{/if}}/>
</div>
<div class="resource-content">
<label class="resource-label-sm">{{localize 'FU.MeleeDamageBonus'}}</label>
<input type="number" name="system.bonuses.damage.melee"
value="{{ system.bonuses.damage.melee }}" data-dtype="Number"
class="resource-inputs bonus" {{#if actor.overrides.system.bonuses.damage.melee
includeZero=true}}disabled
data-tooltip="{{localize "FU.DisabledByActiveEffect"}}"{{/if}}/>
</div>
<div class="resource-content">
<label class="resource-label-sm">{{localize 'FU.RangedDamageBonus'}}</label>
<input type="number" name="system.bonuses.damage.ranged"
value="{{ system.bonuses.damage.ranged }}" data-dtype="Number"
class="resource-inputs bonus" {{#if actor.overrides.system.bonuses.damage.ranged
includeZero=true}}disabled
data-tooltip="{{localize "FU.DisabledByActiveEffect"}}"{{/if}}/>
</div>
<div class="resource-content">
<label class="resource-label-sm">{{localize 'FU.SpellDamageBonus'}}</label>
<input type="number" name="system.bonuses.damage.spell"
value="{{ system.bonuses.damage.spell }}" data-dtype="Number"
class="resource-inputs bonus" {{#if actor.overrides.system.bonuses.damage.spell
includeZero=true}}disabled
data-tooltip="{{localize "FU.DisabledByActiveEffect"}}"{{/if}}/>
</div>

</div>

</fieldset>
Expand Down