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

[#1982] Add support for ranged communication like telepathy #4875

Open
wants to merge 2 commits into
base: 4.2.x
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion dnd5e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ Hooks.once("i18nInit", () => {
DND5E: {
"Feature.Species": game.i18n.localize("DND5E.Feature.SpeciesLegacy"),
FlagsAlertHint: game.i18n.localize("DND5E.FlagsAlertHintLegacy"),
LanguagesExotic: game.i18n.localize("DND5E.LanguagesExoticLegacy"),
"Language.Category.Rare": game.i18n.localize("DND5E.Language.Category.Exotic"),
LongRestHint: game.i18n.localize("DND5E.LongRestHintLegacy"),
LongRestHintGroup: game.i18n.localize("DND5E.LongRestHintGroupLegacy"),
"TARGET.Type.Emanation": foundry.utils.mergeObject(
Expand Down
71 changes: 42 additions & 29 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2328,36 +2328,49 @@
"DND5E.LairAct": "Uses Lair Action",
"DND5E.LairActionLabel": "Lair Action",
"DND5E.LairActionInitiative": "Lair Action Initiative Count",

"DND5E.Language": {
"Label": "Language",
"Category": {
"Exotic": "Exotic Languages",
"Rare": "Rare Languages",
"Standard": "Standard Languages"
},
"Communication": {
"Label": "Ranged Communication",
"Telepathy": "Telepathy"
},
"Dialect": {
"Aarakocra": "Aarakocra",
"Abyssal": "Abyssal",
"Aquan": "Aquan",
"Auran": "Auran",
"Celestial": "Celestial",
"Common": "Common",
"CommonSign": "Common Sign Language",
"DeepSpeech": "Deep Speech",
"Draconic": "Draconic",
"Druidic": "Druidic",
"Dwarvish": "Dwarvish",
"Elvish": "Elvish",
"Giant": "Giant",
"Gith": "Gith",
"Gnoll": "Gnoll",
"Gnomish": "Gnomish",
"Goblin": "Goblin",
"Halfling": "Halfling",
"Ignan": "Ignan",
"Infernal": "Infernal",
"Orc": "Orc",
"Primordial": "Primordial",
"Sylvan": "Sylvan",
"Terran": "Terran",
"ThievesCant": "Thieves' Cant",
"Undercommon": "Undercommon"
}
},
"DND5E.Languages": "Languages",
"DND5E.LanguagesAarakocra": "Aarakocra",
"DND5E.LanguagesAbyssal": "Abyssal",
"DND5E.LanguagesAquan": "Aquan",
"DND5E.LanguagesAuran": "Auran",
"DND5E.LanguagesCelestial": "Celestial",
"DND5E.LanguagesCommon": "Common",
"DND5E.LanguagesCommonSign": "Common Sign Language",
"DND5E.LanguagesDeepSpeech": "Deep Speech",
"DND5E.LanguagesDraconic": "Draconic",
"DND5E.LanguagesDruidic": "Druidic",
"DND5E.LanguagesDwarvish": "Dwarvish",
"DND5E.LanguagesElvish": "Elvish",
"DND5E.LanguagesExotic": "Rare Languages",
"DND5E.LanguagesExoticLegacy": "Exotic Languages",
"DND5E.LanguagesGiant": "Giant",
"DND5E.LanguagesGith": "Gith",
"DND5E.LanguagesGnoll": "Gnoll",
"DND5E.LanguagesGnomish": "Gnomish",
"DND5E.LanguagesGoblin": "Goblin",
"DND5E.LanguagesHalfling": "Halfling",
"DND5E.LanguagesIgnan": "Ignan",
"DND5E.LanguagesInfernal": "Infernal",
"DND5E.LanguagesOrc": "Orc",
"DND5E.LanguagesPrimordial": "Primordial",
"DND5E.LanguagesStandard": "Standard Languages",
"DND5E.LanguagesSylvan": "Sylvan",
"DND5E.LanguagesTerran": "Terran",
"DND5E.LanguagesThievesCant": "Thieves' Cant",
"DND5E.LanguagesUndercommon": "Undercommon",

"DND5E.LegAct": "Legendary Actions",
"DND5E.LegActN.one": "1st Legendary Action",
"DND5E.LegActN.two": "2nd Legendary Action",
Expand Down
1 change: 1 addition & 0 deletions module/applications/actor/_module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export {default as DamagesConfig} from "./config/damages-config.mjs";
export {default as HitDiceConfig} from "./config/hit-dice-config.mjs";
export {default as HitPointsConfig} from "./config/hit-points-config.mjs";
export {default as InitiativeConfig} from "./config/initiative-config.mjs";
export {default as LanguagesConfig} from "./config/languages-config.mjs";
export {default as SkillToolConfig} from "./config/skill-tool-config.mjs";
export {default as SkillsConfig} from "./config/skills-config.mjs";
export {default as SpellSlotsConfig} from "./config/spell-slots-config.mjs";
Expand Down
15 changes: 11 additions & 4 deletions module/applications/actor/base-sheet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import DamagesConfig from "./config/damages-config.mjs";
import HitDiceConfig from "./config/hit-dice-config.mjs";
import HitPointsConfig from "./config/hit-points-config.mjs";
import InitiativeConfig from "./config/initiative-config.mjs";
import LanguagesConfig from "./config/languages-config.mjs";
import SkillToolConfig from "./config/skill-tool-config.mjs";
import SkillsConfig from "./config/skills-config.mjs";
import SpellSlotsConfig from "./config/spell-slots-config.mjs";
Expand Down Expand Up @@ -1239,10 +1240,16 @@ export default class ActorSheet5e extends ActorSheetMixin(ActorSheet) {
event.preventDefault();
const trait = event.currentTarget.dataset.trait;
const options = { document: this.actor, trait };
if ( trait === "tool" ) return new ToolsConfig(options).render({ force: true });
else if ( ["dr", "di", "dv", "dm"].includes(trait) ) return new DamagesConfig(options).render({ force: true });
else if ( trait === "weapon" ) return new WeaponsConfig(options).render({ force: true });
return new TraitsConfig(options).render({ force: true });
switch ( trait ) {
case "di":
case "dm":
case "dr":
case "dv": return new DamagesConfig(options).render({ force: true });
case "languages": return new LanguagesConfig(options).render({ force: true });
case "tool": return new ToolsConfig(options).render({ force: true });
case "weapon": return new WeaponsConfig(options).render({ force: true });
default: return new TraitsConfig(options).render({ force: true });
}
}

/* -------------------------------------------- */
Expand Down
59 changes: 59 additions & 0 deletions module/applications/actor/config/languages-config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import TraitsConfig from "./traits-config.mjs";

/**
* Configuration application for languages.
*/
export default class LanguagesConfig extends TraitsConfig {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["languages"],
trait: "languages"
};

/* -------------------------------------------- */

/** @override */
static PARTS = {
traits: {
template: "systems/dnd5e/templates/actors/config/languages-config.hbs"
}
};

/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */

/** @override */
get title() {
return game.i18n.localize("DND5E.Languages");
}

/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */

/** @inheritDoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);

const unitOptions = Object.entries(CONFIG.DND5E.movementUnits).map(([value, { label }]) => ({ value, label }));
context.communication = Object.entries(CONFIG.DND5E.communicationTypes).map(([key, { label }]) => ({
label, unitOptions,
data: context.data.communication[key],
fields: context.fields.communication.model.fields,
keyPath: `${context.keyPath}.communication.${key}`
}));

return context;
}

/* -------------------------------------------- */
/* Form Submission */
/* -------------------------------------------- */

/** @inheritDoc */
_processFormData(event, form, formData) {
const submitData = super._processFormData(event, form, formData);
return submitData;
}
}
8 changes: 8 additions & 0 deletions module/applications/actor/sheet-v2-mixin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,14 @@ export default function ActorSheetV2Mixin(Base) {
if ( values.length ) traits.dm = values;
}

// Display ranged communication
for ( const [key, { label }] of Object.entries(CONFIG.DND5E.communicationTypes) ) {
const data = this.actor.system.traits?.languages?.communication?.[key];
if ( !data?.value ) return;
traits.languages ??= [];
traits.languages.push({ label: `${label} ${formatDistance(data.value, data.units)}` });
}

// Display weapon masteries
for ( const key of this.actor.system.traits?.weaponProf?.mastery?.value ?? [] ) {
let value = traits.weapon?.find(w => w.key === key);
Expand Down
71 changes: 42 additions & 29 deletions module/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3657,47 +3657,47 @@ DND5E.bloodied = {

/**
* Languages a character can learn.
* @enum {string}
* @enum {object}
*/
DND5E.languages = {
standard: {
label: "DND5E.LanguagesStandard",
label: "DND5E.Language.Category.Standard",
children: {
common: "DND5E.LanguagesCommon",
draconic: "DND5E.LanguagesDraconic",
dwarvish: "DND5E.LanguagesDwarvish",
elvish: "DND5E.LanguagesElvish",
giant: "DND5E.LanguagesGiant",
gnomish: "DND5E.LanguagesGnomish",
goblin: "DND5E.LanguagesGoblin",
halfling: "DND5E.LanguagesHalfling",
orc: "DND5E.LanguagesOrc",
sign: "DND5E.LanguagesCommonSign"
common: "DND5E.Language.Dialect.Common",
draconic: "DND5E.Language.Dialect.Draconic",
dwarvish: "DND5E.Language.Dialect.Dwarvish",
elvish: "DND5E.Language.Dialect.Elvish",
giant: "DND5E.Language.Dialect.Giant",
gnomish: "DND5E.Language.Dialect.Gnomish",
goblin: "DND5E.Language.Dialect.Goblin",
halfling: "DND5E.Language.Dialect.Halfling",
orc: "DND5E.Language.Dialect.Orc",
sign: "DND5E.Language.Dialect.CommonSign"
}
},
exotic: {
label: "DND5E.LanguagesExotic",
label: "DND5E.Language.Category.Rare",
children: {
aarakocra: "DND5E.LanguagesAarakocra",
abyssal: "DND5E.LanguagesAbyssal",
cant: "DND5E.LanguagesThievesCant",
celestial: "DND5E.LanguagesCelestial",
deep: "DND5E.LanguagesDeepSpeech",
druidic: "DND5E.LanguagesDruidic",
gith: "DND5E.LanguagesGith",
gnoll: "DND5E.LanguagesGnoll",
infernal: "DND5E.LanguagesInfernal",
aarakocra: "DND5E.Language.Dialect.Aarakocra",
abyssal: "DND5E.Language.Dialect.Abyssal",
cant: "DND5E.Language.Dialect.ThievesCant",
celestial: "DND5E.Language.Dialect.Celestial",
deep: "DND5E.Language.Dialect.DeepSpeech",
druidic: "DND5E.Language.Dialect.Druidic",
gith: "DND5E.Language.Dialect.Gith",
gnoll: "DND5E.Language.Dialect.Gnoll",
infernal: "DND5E.Language.Dialect.Infernal",
primordial: {
label: "DND5E.LanguagesPrimordial",
label: "DND5E.Language.Dialect.Primordial",
children: {
aquan: "DND5E.LanguagesAquan",
auran: "DND5E.LanguagesAuran",
ignan: "DND5E.LanguagesIgnan",
terran: "DND5E.LanguagesTerran"
aquan: "DND5E.Language.Dialect.Aquan",
auran: "DND5E.Language.Dialect.Auran",
ignan: "DND5E.Language.Dialect.Ignan",
terran: "DND5E.Language.Dialect.Terran"
}
},
sylvan: "DND5E.LanguagesSylvan",
undercommon: "DND5E.LanguagesUndercommon"
sylvan: "DND5E.Language.Dialect.Sylvan",
undercommon: "DND5E.Language.Dialect.Undercommon"
}
}
};
Expand All @@ -3708,6 +3708,19 @@ preLocalize("languages.exotic.children.primordial.children", { sort: true });

/* -------------------------------------------- */

/**
* Communication types that take ranges such as telepathy.
* @enum {{ label: string }}
*/
DND5E.communicationTypes = {
telepathy: {
label: "DND5E.Language.Communication.Telepathy"
}
};
preLocalize("communicationTypes", { key: "label" });

/* -------------------------------------------- */

/**
* Maximum allowed character level.
* @type {number}
Expand Down
21 changes: 12 additions & 9 deletions module/data/actor/character.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import FormulaField from "../fields/formula-field.mjs";
import LocalDocumentField from "../fields/local-document-field.mjs";
import CreatureTypeField from "../shared/creature-type-field.mjs";
import RollConfigField from "../shared/roll-config-field.mjs";
import SimpleTraitField from "./fields/simple-trait-field.mjs";
import AttributesFields from "./templates/attributes.mjs";
import CreatureTemplate from "./templates/creature.mjs";
import DetailsFields from "./templates/details.mjs";
Expand All @@ -14,6 +15,10 @@ const {
ArrayField, BooleanField, HTMLField, IntegerSortField, NumberField, SchemaField, SetField, StringField
} = foundry.data.fields;

/**
* @typedef {import("./fields/simple-trait.mjs").SimpleTraitData} SimpleTraitData
*/

/**
* @typedef {object} ActorFavorites5e
* @property {"activity"|"effect"|"item"|"skill"|"slots"|"tool"} type The favorite type.
Expand Down Expand Up @@ -152,15 +157,13 @@ export default class CharacterData extends CreatureTemplate {
traits: new SchemaField({
...TraitsFields.common,
...TraitsFields.creature,
weaponProf: TraitsFields.makeSimpleTrait({ label: "DND5E.TraitWeaponProf" }, {
extraFields: {
mastery: new SchemaField({
value: new SetField(new StringField()),
bonus: new SetField(new StringField())
})
}
}),
armorProf: TraitsFields.makeSimpleTrait({ label: "DND5E.TraitArmorProf" })
weaponProf: new SimpleTraitField({
mastery: new SchemaField({
value: new SetField(new StringField()),
bonus: new SetField(new StringField())
})
}, { label: "DND5E.TraitWeaponProf" }),
armorProf: new SimpleTraitField({}, { label: "DND5E.TraitArmorProf" })
}, { label: "DND5E.Traits" }),
resources: new SchemaField({
primary: makeResourceField({ label: "DND5E.ResourcePrimary" }),
Expand Down
22 changes: 22 additions & 0 deletions module/data/actor/fields/damage-trait-field.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import SimpleTraitField from "./simple-trait-field.mjs";
const { SetField, StringField } = foundry.data.fields;

/**
* Data structure for a damage actor trait.
*
* @typedef {import("./simple-trait.mjs").SimpleTraitData} DamageTraitData
* @property {Set<string>} bypasses Keys for physical weapon properties that cause resistances to be bypassed.
*/

/**
* Field for storing damage resistances, immunities, and vulnerabilities data.
*/
export default class DamageTraitField extends SimpleTraitField {
constructor(fields={}, { initialBypasses=[], ...options }={}) {
super({
bypasses: new SetField(new StringField(), {
label: "DND5E.DamagePhysicalBypass", hint: "DND5E.DamagePhysicalBypassHint", initial: initialBypasses
})
}, options);
}
}
24 changes: 24 additions & 0 deletions module/data/actor/fields/simple-trait-field.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const { SchemaField, SetField, StringField } = foundry.data.fields;

/**
* Data structure for a standard actor trait.
*
* @typedef {object} SimpleTraitData
* @property {Set<string>} value Keys for currently selected traits.
* @property {string} custom Semicolon-separated list of custom traits.
*/

/**
* Field for storing standard trait data.
*/
export default class SimpleTraitField extends SchemaField {
constructor(fields={}, { initialValue=[], ...options }={}) {
fields = {
value: new SetField(new StringField(), { label: "DND5E.TraitsChosen", initial: initialValue }),
custom: new StringField({ required: true, label: "DND5E.Special" }),
...fields
};
Object.entries(fields).forEach(([k, v]) => !v ? delete fields[k] : null);
super(fields, options);
}
}
Loading