Initial skeleton

This commit is contained in:
2026-03-05 21:51:31 +01:00
commit 12458925a1
53 changed files with 6646 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules/
css/
packs/
Binary file not shown.
Binary file not shown.
+72
View File
@@ -0,0 +1,72 @@
import { SYSTEM } from "./module/config/system.mjs"
globalThis.SYSTEM = SYSTEM
import * as models from "./module/models/_module.mjs"
import * as documents from "./module/documents/_module.mjs"
import * as applications from "./module/applications/_module.mjs"
Hooks.once("init", function () {
console.info("Adventures with Emmy | Initializing System")
console.info(SYSTEM.ASCII)
globalThis.adventuresWithEmmy = game.system
game.system.CONST = SYSTEM
game.system.api = { applications, models, documents }
CONFIG.Actor.documentClass = documents.AwEActor
CONFIG.Actor.dataModels = {
character: models.AwECharacter,
creature: models.AwECreature
}
CONFIG.Item.documentClass = documents.AwEItem
CONFIG.Item.dataModels = {
ability: models.AwEAbility,
field: models.AwEField,
archetype: models.AwEArchetype,
background: models.AwEBackground,
kit: models.AwEKit,
weapon: models.AwEWeapon,
equipment: models.AwEEquipment
}
// Register actor sheets
foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
foundry.documents.collections.Actors.registerSheet("fvtt-adventures-with-emmy", applications.AwECharacterSheet, {
types: ["character"], makeDefault: true
})
foundry.documents.collections.Actors.registerSheet("fvtt-adventures-with-emmy", applications.AwECreatureSheet, {
types: ["creature"], makeDefault: true
})
// Register item sheets
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet)
foundry.documents.collections.Items.registerSheet("fvtt-adventures-with-emmy", applications.AwEAbilitySheet, {
types: ["ability"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("fvtt-adventures-with-emmy", applications.AwEFieldSheet, {
types: ["field"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("fvtt-adventures-with-emmy", applications.AwEArchetypeSheet, {
types: ["archetype"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("fvtt-adventures-with-emmy", applications.AwEBackgroundSheet, {
types: ["background"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("fvtt-adventures-with-emmy", applications.AwEKitSheet, {
types: ["kit"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("fvtt-adventures-with-emmy", applications.AwEWeaponSheet, {
types: ["weapon"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("fvtt-adventures-with-emmy", applications.AwEEquipmentSheet, {
types: ["equipment"], makeDefault: true
})
CONFIG.ChatMessage.documentClass = documents.AwEChatMessage
})
Hooks.once("ready", function () {
console.info("Adventures with Emmy | System Ready")
})
View File
+219
View File
@@ -0,0 +1,219 @@
import jsdoc from 'eslint-plugin-jsdoc';
import prettier from 'eslint-plugin-prettier';
import configPrettier from 'eslint-config-prettier';
export default [
{
ignores: [
"node_modules/",
"eslint.config.mjs",
"build.mjs",
"gulpfile.js"
],
languageOptions: {
globals: {
browser: true,
es2022: true,
node: true,
jquery: true,
},
ecmaVersion: 2022,
sourceType: 'module',
},
plugins: {
jsdoc,
prettier
},
rules: {
'array-bracket-spacing': ['warn', 'never'],
'array-callback-return': 'warn',
'arrow-spacing': 'warn',
'comma-dangle': ['warn', 'never'],
'comma-style': 'warn',
'computed-property-spacing': 'warn',
'constructor-super': 'error',
'default-param-last': 'warn',
'dot-location': ['warn', 'property'],
'eol-last': ['error', 'always'],
'eqeqeq': ['warn', 'smart'],
'func-call-spacing': 'warn',
'func-names': ['warn', 'never'],
'getter-return': 'warn',
'lines-between-class-members': 'warn',
'new-parens': ['warn', 'always'],
'no-alert': 'warn',
'no-array-constructor': 'warn',
'no-class-assign': 'warn',
'no-compare-neg-zero': 'warn',
'no-cond-assign': 'warn',
'no-const-assign': 'error',
'no-constant-condition': 'warn',
'no-constructor-return': 'warn',
'no-delete-var': 'warn',
'no-dupe-args': 'warn',
'no-dupe-class-members': 'warn',
'no-dupe-keys': 'warn',
'no-duplicate-case': 'warn',
'no-duplicate-imports': ['warn', { includeExports: true }],
'no-empty': ['warn', { allowEmptyCatch: true }],
'no-empty-character-class': 'warn',
'no-empty-pattern': 'warn',
'no-func-assign': 'warn',
'no-global-assign': 'warn',
'no-implicit-coercion': ['warn', { allow: ['!!'] }],
'no-implied-eval': 'warn',
'no-import-assign': 'warn',
'no-invalid-regexp': 'warn',
'no-irregular-whitespace': 'warn',
'no-iterator': 'warn',
'no-lone-blocks': 'warn',
'no-lonely-if': 'off',
'no-loop-func': 'warn',
'no-misleading-character-class': 'warn',
'no-mixed-operators': 'warn',
'no-multi-str': 'warn',
'no-multiple-empty-lines': 'warn',
'no-new-func': 'warn',
'no-new-object': 'warn',
'no-new-symbol': 'warn',
'no-new-wrappers': 'warn',
'no-nonoctal-decimal-escape': 'warn',
'no-obj-calls': 'warn',
'no-octal': 'warn',
'no-octal-escape': 'warn',
'no-promise-executor-return': 'warn',
'no-proto': 'warn',
'no-regex-spaces': 'warn',
'no-script-url': 'warn',
'no-self-assign': 'warn',
'no-self-compare': 'warn',
'no-setter-return': 'warn',
'no-sequences': 'warn',
'no-template-curly-in-string': 'warn',
'no-this-before-super': 'error',
'no-unexpected-multiline': 'warn',
'no-unmodified-loop-condition': 'warn',
'no-unneeded-ternary': 'warn',
'no-unreachable': 'warn',
'no-unreachable-loop': 'warn',
'no-unsafe-negation': ['warn', { enforceForOrderingRelations: true }],
'no-unsafe-optional-chaining': ['warn', { disallowArithmeticOperators: true }],
'no-unused-expressions': 'warn',
'no-useless-backreference': 'warn',
'no-useless-call': 'warn',
'no-useless-catch': 'warn',
'no-useless-computed-key': ['warn', { enforceForClassMembers: true }],
'no-useless-concat': 'warn',
'no-useless-constructor': 'warn',
'no-useless-rename': 'warn',
'no-useless-return': 'warn',
'no-var': 'warn',
'no-void': 'warn',
'no-whitespace-before-property': 'warn',
'prefer-numeric-literals': 'warn',
'prefer-object-spread': 'warn',
'prefer-regex-literals': 'warn',
'prefer-spread': 'warn',
'rest-spread-spacing': ['warn', 'never'],
'semi-spacing': 'warn',
'semi-style': ['warn', 'last'],
'space-unary-ops': ['warn', { words: true, nonwords: false }],
'switch-colon-spacing': 'warn',
'symbol-description': 'warn',
'template-curly-spacing': ['warn', 'never'],
'unicode-bom': ['warn', 'never'],
'use-isnan': ['warn', { enforceForSwitchCase: true, enforceForIndexOf: true }],
'valid-typeof': ['warn', { requireStringLiterals: true }],
'wrap-iife': ['warn', 'inside'],
'arrow-parens': ['warn', 'as-needed', { requireForBlockBody: false }],
'capitalized-comments': ['warn', 'always', {
ignoreConsecutiveComments: true,
ignorePattern: 'noinspection',
}],
'comma-spacing': 'warn',
'dot-notation': 'warn',
indent: ['warn', 2, { SwitchCase: 1 }],
'key-spacing': 'warn',
'keyword-spacing': ['warn', { overrides: { catch: { before: true, after: false } } }],
'max-len': ['warn', {
code: 180,
ignoreTrailingComments: true,
ignoreUrls: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
}],
'no-extra-boolean-cast': ['warn', { enforceForLogicalOperands: true }],
'no-multi-spaces': ['warn', { ignoreEOLComments: true }],
'no-tabs': 'warn',
'no-throw-literal': 'error',
'no-trailing-spaces': 'warn',
'no-useless-escape': 'warn',
'nonblock-statement-body-position': ['warn', 'beside'],
'one-var': ['warn', 'never'],
'operator-linebreak': ['warn', 'before', {
overrides: { '=': 'after', '+=': 'after', '-=': 'after' },
}],
'prefer-template': 'warn',
'quote-props': ['warn', 'as-needed', { keywords: false }],
quotes: ['warn', 'double', { avoidEscape: true, allowTemplateLiterals: false }],
semi: ["error", "never"],
'spaced-comment': 'warn',
'jsdoc/check-access': 'warn',
'jsdoc/check-alignment': 'warn',
'jsdoc/check-examples': 'off',
'jsdoc/check-indentation': 'off',
'jsdoc/check-line-alignment': 'off',
'jsdoc/check-param-names': 'warn',
'jsdoc/check-property-names': 'warn',
'jsdoc/check-syntax': 'off',
'jsdoc/check-tag-names': ['warn', { definedTags: ['category'] }],
'jsdoc/check-types': 'warn',
'jsdoc/check-values': 'warn',
'jsdoc/empty-tags': 'warn',
'jsdoc/implements-on-classes': 'warn',
'jsdoc/match-description': 'off',
'jsdoc/newline-after-description': 'off',
'jsdoc/no-bad-blocks': 'warn',
'jsdoc/no-defaults': 'off',
'jsdoc/no-types': 'off',
'jsdoc/no-undefined-types': 'off',
'jsdoc/require-description': 'warn',
'jsdoc/require-description-complete-sentence': 'off',
'jsdoc/require-example': 'off',
'jsdoc/require-file-overview': 'off',
'jsdoc/require-hyphen-before-param-description': ['warn', 'never'],
'jsdoc/require-jsdoc': 'warn',
'jsdoc/require-param': 'warn',
'jsdoc/require-param-description': 'off',
'jsdoc/require-param-name': 'warn',
'jsdoc/require-param-type': 'warn',
'jsdoc/require-property': 'warn',
'jsdoc/require-property-description': 'off',
'jsdoc/require-property-name': 'warn',
'jsdoc/require-property-type': 'warn',
'jsdoc/require-returns': 'off',
'jsdoc/require-returns-check': 'warn',
'jsdoc/require-returns-description': 'off',
'jsdoc/require-returns-type': 'warn',
'jsdoc/require-throws': 'off',
'jsdoc/require-yields': 'warn',
'jsdoc/require-yields-check': 'warn',
'jsdoc/valid-types': 'off',
},
settings: {
jsdoc: {
preferredTypes: {
'.<>': '<>',
object: 'Object',
Object: 'object',
},
mode: 'typescript',
tagNamePreference: {
augments: 'extends',
},
},
},
},
// Ajout de la configuration Prettier qui désactive les règles ESLint en conflit avec Prettier
configPrettier
];
+32
View File
@@ -0,0 +1,32 @@
const gulp = require('gulp');
const less = require('gulp-less');
/* ----------------------------------------- */
/* Compile LESS
/* ----------------------------------------- */
function compileLESS() {
return gulp.src("styles/adventures-with-emmy.less")
.pipe(less())
.pipe(gulp.dest("./css"))
}
const css = gulp.series(compileLESS);
/* ----------------------------------------- */
/* Watch Updates
/* ----------------------------------------- */
const SIMPLE_LESS = ["styles/*.less"];
function watchUpdates() {
gulp.watch(SIMPLE_LESS, css);
}
/* ----------------------------------------- */
/* Export Tasks
/* ----------------------------------------- */
exports.default = gulp.series(
gulp.parallel(css),
watchUpdates
);
exports.css = css;
exports.watchUpdates = watchUpdates;
+70
View File
@@ -0,0 +1,70 @@
{
"AWEMMY.Actor.Character": "Hero",
"AWEMMY.Actor.Creature": "Creature",
"AWEMMY.Attribute.Agility": "Agility",
"AWEMMY.Attribute.Fitness": "Fitness",
"AWEMMY.Attribute.Awareness": "Awareness",
"AWEMMY.Attribute.Influence": "Influence",
"AWEMMY.Attribute.AGI": "AGI",
"AWEMMY.Attribute.FIT": "FIT",
"AWEMMY.Attribute.AWA": "AWA",
"AWEMMY.Attribute.INF": "INF",
"AWEMMY.Condition.Edge": "Edge",
"AWEMMY.Condition.Hampered": "Hampered",
"AWEMMY.Condition.Inhibited": "Inhibited",
"AWEMMY.Condition.Jumbled": "Jumbled",
"AWEMMY.Condition.Mishap": "Mishap",
"AWEMMY.Condition.Prone": "Prone",
"AWEMMY.Condition.Quickened": "Quickened",
"AWEMMY.Condition.Slowed": "Slowed",
"AWEMMY.Condition.Vulnerable": "Vulnerable",
"AWEMMY.Ability.Cost.Free": "Free",
"AWEMMY.Ability.Type.Field": "Field",
"AWEMMY.Ability.Type.Archetype": "Archetype",
"AWEMMY.Ability.Type.General": "General",
"AWEMMY.Ability.Type.Beginner": "Beginner",
"AWEMMY.Item.Ability": "Ability",
"AWEMMY.Item.Field": "Field",
"AWEMMY.Item.Archetype": "Archetype",
"AWEMMY.Item.Background": "Background",
"AWEMMY.Item.Kit": "Kit",
"AWEMMY.Item.Weapon": "Weapon",
"AWEMMY.Item.Equipment": "Equipment",
"AWEMMY.Roll.CriticalSuccess": "Critical Success",
"AWEMMY.Roll.Success": "Success",
"AWEMMY.Roll.Failure": "Failure",
"AWEMMY.Roll.CriticalFailure": "Critical Failure",
"AWEMMY.Sheet.Tab.Main": "Main",
"AWEMMY.Sheet.Tab.Biography": "Biography",
"AWEMMY.Sheet.Tab.Equipment": "Equipment",
"AWEMMY.Sheet.EditMode": "Edit",
"AWEMMY.Sheet.PlayMode": "Play",
"AWEMMY.Character.Level": "Level",
"AWEMMY.Character.Stride": "Stride",
"AWEMMY.Character.HP": "HP",
"AWEMMY.Character.FlowPoints": "Flow Points",
"AWEMMY.Character.BoostLevel": "Boost Level",
"AWEMMY.Character.Mod": "MOD",
"AWEMMY.Character.DC": "DC",
"AWEMMY.Creature.EurekaRubric": "Eureka Rubric",
"AWEMMY.Creature.Claims": "Claims",
"AWEMMY.Creature.Evidence": "Evidence",
"AWEMMY.Creature.Hints": "Hints",
"AWEMMY.Ability.Cost": "Cost",
"AWEMMY.Ability.Frequency": "Frequency",
"AWEMMY.Ability.Requirements": "Requirements",
"AWEMMY.Ability.Trigger": "Trigger",
"AWEMMY.Ability.Traits": "Traits",
"AWEMMY.Weapon.Range": "Range",
"AWEMMY.Weapon.Damage": "Damage",
"AWEMMY.Weapon.DamageType": "Damage Type",
"AWEMMY.Weapon.AttackAttribute": "Attack Attribute",
"AWEMMY.Field.KeyAttribute": "Key Attribute",
"AWEMMY.Field.Specializations": "Specializations",
"AWEMMY.Field.KnowledgeBonus": "Knowledge Bonus",
"AWEMMY.Background.Bonus": "Background Bonus",
"AWEMMY.Kit.Field": "Field",
"AWEMMY.Kit.Charges": "Charges",
"AWEMMY.Equipment.Quantity": "Quantity",
"AWEMMY.Equipment.Weight": "Weight"
}
+9
View File
@@ -0,0 +1,9 @@
export { default as AwECharacterSheet } from "./sheets/character-sheet.mjs"
export { default as AwECreatureSheet } from "./sheets/creature-sheet.mjs"
export { default as AwEAbilitySheet } from "./sheets/ability-sheet.mjs"
export { default as AwEFieldSheet } from "./sheets/field-sheet.mjs"
export { default as AwEArchetypeSheet } from "./sheets/archetype-sheet.mjs"
export { default as AwEBackgroundSheet } from "./sheets/background-sheet.mjs"
export { default as AwEKitSheet } from "./sheets/kit-sheet.mjs"
export { default as AwEWeaponSheet } from "./sheets/weapon-sheet.mjs"
export { default as AwEEquipmentSheet } from "./sheets/equipment-sheet.mjs"
@@ -0,0 +1,17 @@
import AwEItemSheet from "./base-item-sheet.mjs"
export default class AwEAbilitySheet extends AwEItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["ability"],
position: { width: 620 },
window: { contentClasses: ["ability-content"] }
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-adventures-with-emmy/templates/ability.hbs"
}
}
}
@@ -0,0 +1,17 @@
import AwEItemSheet from "./base-item-sheet.mjs"
export default class AwEArchetypeSheet extends AwEItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["archetype"],
position: { width: 620 },
window: { contentClasses: ["archetype-content"] }
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-adventures-with-emmy/templates/archetype.hbs"
}
}
}
@@ -0,0 +1,17 @@
import AwEItemSheet from "./base-item-sheet.mjs"
export default class AwEBackgroundSheet extends AwEItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["background"],
position: { width: 620 },
window: { contentClasses: ["background-content"] }
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-adventures-with-emmy/templates/background.hbs"
}
}
}
@@ -0,0 +1,231 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class AwEActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
/**
* Different sheet modes.
* @enum {number}
*/
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#dragDrop
/** @override */
static DEFAULT_OPTIONS = {
classes: ["awemmy", "actor"],
position: {
width: 900,
height: "auto"
},
form: {
submitOnChange: true
},
window: {
resizable: true
},
dragDrop: [{ dragSelector: '[data-drag="true"], .rollable', dropSelector: null }],
actions: {
editImage: AwEActorSheet.#onEditImage,
toggleSheet: AwEActorSheet.#onToggleSheet,
edit: AwEActorSheet.#onItemEdit,
delete: AwEActorSheet.#onItemDelete
}
}
/**
* The current sheet mode.
* @type {number}
*/
_sheetMode = this.constructor.SHEET_MODES.PLAY
/**
* Is the sheet currently in 'Play' mode?
* @type {boolean}
*/
get isPlayMode() {
return this._sheetMode === this.constructor.SHEET_MODES.PLAY
}
/**
* Is the sheet currently in 'Edit' mode?
* @type {boolean}
*/
get isEditMode() {
return this._sheetMode === this.constructor.SHEET_MODES.EDIT
}
/** @override */
async _prepareContext() {
const context = {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
actor: this.document,
system: this.document.system,
source: this.document.toObject(),
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable
}
return context
}
/** @override */
_onRender(context, options) {
this.#dragDrop.forEach(d => d.bind(this.element))
const rollables = this.element.querySelectorAll(".rollable")
rollables.forEach(d => d.addEventListener("click", this._onRoll.bind(this)))
}
/**
* Handle rolling an attribute check.
* @param {PointerEvent} event - The click event.
*/
async _onRoll(event) {
if (this.isEditMode) return
const attributeId = event.currentTarget.dataset.attributeId
if (!attributeId) return
await this.document.rollAttribute(attributeId)
}
// #region Drag-and-Drop Workflow
/**
* Create drag-and-drop workflow handlers for this Application.
* @returns {DragDrop[]} An array of DragDrop handlers.
* @private
*/
#createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this)
}
d.callbacks = {
dragstart: this._onDragStart.bind(this),
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
}
/**
* Callback actions which occur when a dragged element is dropped on a target.
* @param {DragEvent} event - The originating DragEvent.
* @protected
*/
async _onDrop(event) {}
/**
* Define whether a user is able to begin a dragstart workflow for a given drag selector.
* @param {string} selector - The candidate HTML selector for dragging.
* @returns {boolean} Can the current user drag this selector?
* @protected
*/
_canDragStart(selector) {
return this.isEditable
}
/**
* Define whether a user is able to conclude a drag-and-drop workflow for a given drop selector.
* @param {string} selector - The candidate HTML selector for the drop target.
* @returns {boolean} Can the current user drop on this selector?
* @protected
*/
_canDragDrop(selector) {
return this.isEditable && this.document.isOwner
}
/**
* Callback actions which occur at the beginning of a drag start workflow.
* @param {DragEvent} event - The originating DragEvent.
* @protected
*/
_onDragStart(event) {
if ("link" in event.target.dataset) return
}
/**
* Callback actions which occur when a dragged element is over a drop target.
* @param {DragEvent} event - The originating DragEvent.
* @protected
*/
_onDragOver(event) {}
/**
* Drop an item onto the actor.
* @param {Item} item - The item being dropped.
*/
async _onDropItem(item) {
const itemData = item.toObject()
await this.document.createEmbeddedDocuments("Item", [itemData], { renderSheet: false })
}
// #endregion
// #region Actions
/**
* Handle toggling between Edit and Play mode.
* @param {Event} event - The initiating click event.
* @param {HTMLElement} target - The current target of the event listener.
*/
static #onToggleSheet(event, target) {
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()
}
/**
* Handle changing a Document's image.
* @param {PointerEvent} event - The originating click event.
* @param {HTMLElement} target - The capturing HTML element which defined a [data-action].
* @returns {Promise} The file picker promise.
* @private
*/
static async #onEditImage(event, target) {
const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr)
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}
const fp = new FilePicker({
current,
type: "image",
redirectToRoot: img ? [img] : [],
callback: path => {
this.document.update({ [attr]: path })
},
top: this.position.top + 40,
left: this.position.left + 10
})
return fp.browse()
}
/**
* Edit an existing item within the Actor.
* @param {PointerEvent} event - The originating click event.
* @param {HTMLElement} target - The capturing HTML element which defined a [data-action].
*/
static async #onItemEdit(event, target) {
const id = target.getAttribute("data-item-id")
const uuid = target.getAttribute("data-item-uuid")
let item = await fromUuid(uuid)
if (!item) item = this.document.items.get(id)
if (!item) return
item.sheet.render(true)
}
/**
* Delete an existing item within the Actor.
* @param {PointerEvent} event - The originating click event.
* @param {HTMLElement} target - The capturing HTML element which defined a [data-action].
*/
static async #onItemDelete(event, target) {
const itemUuid = target.getAttribute("data-item-uuid")
const item = await fromUuid(itemUuid)
await item.deleteDialog()
}
// #endregion
}
@@ -0,0 +1,185 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class AwEItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
/**
* Different sheet modes.
* @enum {number}
*/
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#dragDrop
/** @override */
static DEFAULT_OPTIONS = {
classes: ["awemmy", "item"],
position: {
width: 600,
height: "auto"
},
form: {
submitOnChange: true
},
window: {
resizable: true
},
dragDrop: [{ dragSelector: "[data-drag]", dropSelector: null }],
actions: {
toggleSheet: AwEItemSheet.#onToggleSheet,
editImage: AwEItemSheet.#onEditImage
}
}
/**
* The current sheet mode.
* @type {number}
*/
_sheetMode = this.constructor.SHEET_MODES.PLAY
/**
* Is the sheet currently in 'Play' mode?
* @type {boolean}
*/
get isPlayMode() {
return this._sheetMode === this.constructor.SHEET_MODES.PLAY
}
/**
* Is the sheet currently in 'Edit' mode?
* @type {boolean}
*/
get isEditMode() {
return this._sheetMode === this.constructor.SHEET_MODES.EDIT
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.fields = this.document.schema.fields
context.systemFields = this.document.system.schema.fields
context.item = this.document
context.system = this.document.system
context.source = this.document.toObject()
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.description, { async: true }
)
context.isEditMode = this.isEditMode
context.isPlayMode = this.isPlayMode
context.isEditable = this.isEditable
return context
}
/** @override */
_onRender(context, options) {
super._onRender(context, options)
this.#dragDrop.forEach(d => d.bind(this.element))
}
// #region Drag-and-Drop Workflow
/**
* Create drag-and-drop workflow handlers for this Application.
* @returns {DragDrop[]} An array of DragDrop handlers.
* @private
*/
#createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this)
}
d.callbacks = {
dragstart: this._onDragStart.bind(this),
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
}
/**
* Define whether a user is able to begin a dragstart workflow for a given drag selector.
* @param {string} selector - The candidate HTML selector for dragging.
* @returns {boolean} Can the current user drag this selector?
* @protected
*/
_canDragStart(selector) {
return this.isEditable
}
/**
* Define whether a user is able to conclude a drag-and-drop workflow for a given drop selector.
* @param {string} selector - The candidate HTML selector for the drop target.
* @returns {boolean} Can the current user drop on this selector?
* @protected
*/
_canDragDrop(selector) {
return this.isEditable && this.document.isOwner
}
/**
* Callback actions which occur at the beginning of a drag start workflow.
* @param {DragEvent} event - The originating DragEvent.
* @protected
*/
_onDragStart(event) {
if ("link" in event.target.dataset) return
}
/**
* Callback actions which occur when a dragged element is over a drop target.
* @param {DragEvent} event - The originating DragEvent.
* @protected
*/
_onDragOver(event) {}
/**
* Callback actions which occur when a dragged element is dropped on a target.
* @param {DragEvent} event - The originating DragEvent.
* @protected
*/
async _onDrop(event) {}
// #endregion
// #region Actions
/**
* Handle toggling between Edit and Play mode.
* @param {Event} event - The initiating click event.
* @param {HTMLElement} target - The current target of the event listener.
*/
static #onToggleSheet(event, target) {
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()
}
/**
* Handle changing a Document's image.
* @param {PointerEvent} event - The originating click event.
* @param {HTMLElement} target - The capturing HTML element which defined a [data-action].
* @returns {Promise} The file picker promise.
* @private
*/
static async #onEditImage(event, target) {
const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr)
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}
const fp = new FilePicker({
current,
type: "image",
redirectToRoot: img ? [img] : [],
callback: path => {
this.document.update({ [attr]: path })
},
top: this.position.top + 40,
left: this.position.left + 10
})
return fp.browse()
}
// #endregion
}
@@ -0,0 +1,161 @@
import AwEActorSheet from "./base-actor-sheet.mjs"
export default class AwECharacterSheet extends AwEActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["character"],
position: {
width: 960,
height: 780
},
window: {
contentClasses: ["character-content"]
},
actions: {
createAbility: AwECharacterSheet.#onCreateAbility,
createWeapon: AwECharacterSheet.#onCreateWeapon,
createKit: AwECharacterSheet.#onCreateKit,
createEquipment: AwECharacterSheet.#onCreateEquipment,
flowPointsPlus: AwECharacterSheet.#onFlowPointsPlus,
flowPointsMinus: AwECharacterSheet.#onFlowPointsMinus
}
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-adventures-with-emmy/templates/character-main.hbs"
},
tabs: {
template: "templates/generic/tab-navigation.hbs"
},
biography: {
template: "systems/fvtt-adventures-with-emmy/templates/character-biography.hbs"
},
equipment: {
template: "systems/fvtt-adventures-with-emmy/templates/character-equipment.hbs"
}
}
/** @override */
tabGroups = {
sheet: "main"
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>} The tab objects.
*/
#getTabs() {
const tabs = {
main: { id: "main", group: "sheet", icon: "fa-solid fa-user", label: "AWEMMY.Sheet.Tab.Main" },
biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "AWEMMY.Sheet.Tab.Biography" },
equipment: { id: "equipment", group: "sheet", icon: "fa-solid fa-backpack", label: "AWEMMY.Sheet.Tab.Equipment" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
/** @override */
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
context.tab = context.tabs.main
context.abilities = doc.itemTypes.ability
break
case "biography":
context.tab = context.tabs.biography
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.description, { async: true }
)
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.notes, { async: true }
)
break
case "equipment":
context.tab = context.tabs.equipment
context.kits = doc.itemTypes.kit
context.weapons = doc.itemTypes.weapon
context.equipments = doc.itemTypes.equipment
break
}
return context
}
/** @override */
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (data.type === "Item") {
const item = await fromUuid(data.uuid)
return this._onDropItem(item)
}
}
/**
* Create a new ability item.
* @param {Event} event - The triggering event.
* @param {HTMLElement} target - The target element.
*/
static #onCreateAbility(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: "New Ability", type: "ability" }])
}
/**
* Create a new weapon item.
* @param {Event} event - The triggering event.
* @param {HTMLElement} target - The target element.
*/
static #onCreateWeapon(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: "New Weapon", type: "weapon" }])
}
/**
* Create a new kit item.
* @param {Event} event - The triggering event.
* @param {HTMLElement} target - The target element.
*/
static #onCreateKit(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: "New Kit", type: "kit" }])
}
/**
* Create a new equipment item.
* @param {Event} event - The triggering event.
* @param {HTMLElement} target - The target element.
*/
static #onCreateEquipment(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: "New Equipment", type: "equipment" }])
}
/**
* Increase flow points by 1.
* @param {Event} event - The triggering event.
* @param {HTMLElement} target - The target element.
*/
static #onFlowPointsPlus(event, target) {
const current = this.actor.system.flowPoints.value
this.actor.update({ "system.flowPoints.value": current + 1 })
}
/**
* Decrease flow points by 1.
* @param {Event} event - The triggering event.
* @param {HTMLElement} target - The target element.
*/
static #onFlowPointsMinus(event, target) {
const current = this.actor.system.flowPoints.value
this.actor.update({ "system.flowPoints.value": Math.max(0, current - 1) })
}
}
@@ -0,0 +1,31 @@
import AwEActorSheet from "./base-actor-sheet.mjs"
export default class AwECreatureSheet extends AwEActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["creature"],
position: {
width: 700,
height: "auto"
},
window: {
contentClasses: ["creature-content"]
}
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-adventures-with-emmy/templates/creature-main.hbs"
}
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.description, { async: true }
)
return context
}
}
@@ -0,0 +1,17 @@
import AwEItemSheet from "./base-item-sheet.mjs"
export default class AwEEquipmentSheet extends AwEItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["equipment"],
position: { width: 620 },
window: { contentClasses: ["equipment-content"] }
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-adventures-with-emmy/templates/equipment.hbs"
}
}
}
@@ -0,0 +1,17 @@
import AwEItemSheet from "./base-item-sheet.mjs"
export default class AwEFieldSheet extends AwEItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["field"],
position: { width: 620 },
window: { contentClasses: ["field-content"] }
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-adventures-with-emmy/templates/field.hbs"
}
}
}
+17
View File
@@ -0,0 +1,17 @@
import AwEItemSheet from "./base-item-sheet.mjs"
export default class AwEKitSheet extends AwEItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["kit"],
position: { width: 620 },
window: { contentClasses: ["kit-content"] }
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-adventures-with-emmy/templates/kit.hbs"
}
}
}
@@ -0,0 +1,17 @@
import AwEItemSheet from "./base-item-sheet.mjs"
export default class AwEWeaponSheet extends AwEItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["weapon"],
position: { width: 620 },
window: { contentClasses: ["weapon-content"] }
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-adventures-with-emmy/templates/weapon.hbs"
}
}
}
+65
View File
@@ -0,0 +1,65 @@
export const SYSTEM_ID = "fvtt-adventures-with-emmy"
export const DEV_MODE = false
export const ATTRIBUTES = {
agility: { id: "agility", abbrev: "AGI", label: "AWEMMY.Attribute.Agility" },
fitness: { id: "fitness", abbrev: "FIT", label: "AWEMMY.Attribute.Fitness" },
awareness: { id: "awareness", abbrev: "AWA", label: "AWEMMY.Attribute.Awareness" },
influence: { id: "influence", abbrev: "INF", label: "AWEMMY.Attribute.Influence" }
}
export const CONDITIONS = {
edge: { id: "edge", label: "AWEMMY.Condition.Edge" },
hampered: { id: "hampered", label: "AWEMMY.Condition.Hampered" },
inhibited: { id: "inhibited", label: "AWEMMY.Condition.Inhibited" },
jumbled: { id: "jumbled", label: "AWEMMY.Condition.Jumbled" },
mishap: { id: "mishap", label: "AWEMMY.Condition.Mishap" },
prone: { id: "prone", label: "AWEMMY.Condition.Prone" },
quickened: { id: "quickened", label: "AWEMMY.Condition.Quickened" },
slowed: { id: "slowed", label: "AWEMMY.Condition.Slowed" },
vulnerable: { id: "vulnerable", label: "AWEMMY.Condition.Vulnerable" }
}
export const ABILITY_COST = {
"one": { id: "one", label: "Δ" },
"two": { id: "two", label: "ΔΔ" },
"three": { id: "three", label: "ΔΔΔ" },
"reaction": { id: "reaction", label: "↩" },
"free": { id: "free", label: "AWEMMY.Ability.Cost.Free" },
"none": { id: "none", label: "—" }
}
export const ABILITY_TYPE = {
"field": { id: "field", label: "AWEMMY.Ability.Type.Field" },
"archetype": { id: "archetype", label: "AWEMMY.Ability.Type.Archetype" },
"general": { id: "general", label: "AWEMMY.Ability.Type.General" },
"beginner": { id: "beginner", label: "AWEMMY.Ability.Type.Beginner" }
}
export const OUTCOME_LABELS = {
criticalSuccess: "AWEMMY.Roll.CriticalSuccess",
success: "AWEMMY.Roll.Success",
failure: "AWEMMY.Roll.Failure",
criticalFailure: "AWEMMY.Roll.CriticalFailure"
}
export const ASCII = `
_ _ _ _ _ _ _____
/ \\ __| |_ _____ _ __ | |_ _ _ _ __ ___ ___| | | | | | ____|_ __ ___ _ __ ___ _ _
/ _ \\ / _\` \\ \\ / / _ \\ '_ \\| __| | | | '__/ _ \\/ __| | | | | | _| | '_ \` _ \\| '_ \` _ \\| | | |
/ ___ \\ (_| |\\ V / __/ | | | |_| |_| | | | __/\\__ \\ |_| | |___| |___| | | | | | | | | | | |_| |
/_/ \\_\\__,_| \\_/ \\___|_| |_|\\__|\\__,_|_| \\___||___/\\___/|_____|_____|_| |_| |_|_| |_| |_|\\__, |
|___/
`
// Re-export all for convenience
export const SYSTEM = {
SYSTEM_ID,
DEV_MODE,
ATTRIBUTES,
CONDITIONS,
ABILITY_COST,
ABILITY_TYPE,
OUTCOME_LABELS,
ASCII
}
+4
View File
@@ -0,0 +1,4 @@
export { default as AwEActor } from "./actor.mjs"
export { default as AwEItem } from "./item.mjs"
export { default as AwERoll } from "./roll.mjs"
export { default as AwEChatMessage } from "./chat-message.mjs"
+70
View File
@@ -0,0 +1,70 @@
export default class AwEActor extends Actor {
/** @override */
prepareData() {
super.prepareData()
}
/** @override */
prepareBaseData() {}
/** @override */
prepareDerivedData() {
const actorData = this
this._prepareCharacterData(actorData)
this._prepareCreatureData(actorData)
}
/**
* Prepare character data.
* @param {object} actorData - The actor data.
*/
_prepareCharacterData(actorData) {
if (actorData.type !== "character") return
}
/**
* Prepare creature data.
* @param {object} actorData - The actor data.
*/
_prepareCreatureData(actorData) {
if (actorData.type !== "creature") return
}
/**
* Roll an attribute check.
* @param {string} attributeId - The attribute to roll.
* @param {object} options - Roll options.
* @returns {Promise<Roll>} The roll result.
*/
async rollAttribute(attributeId, options = {}) {
const attribute = this.system.attributes[attributeId]
if (!attribute) return null
const mod = attribute.mod ?? 0
const formula = `1d20 + ${mod}`
const roll = new Roll(formula)
await roll.evaluate()
// Determine outcome vs DC if provided
let outcome = null
if (options.dc !== undefined) {
const total = roll.total
const dc = options.dc
if (total >= dc + 10) outcome = "criticalSuccess"
else if (total >= dc) outcome = "success"
else if (total <= dc - 10) outcome = "criticalFailure"
else outcome = "failure"
}
// Send to chat
const attrLabel = attributeId.charAt(0).toUpperCase() + attributeId.slice(1)
const flavor = options.flavor || game.i18n.localize(`AWEMMY.Attribute.${attrLabel}`)
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor,
rollMode: game.settings.get("core", "rollMode")
})
return { roll, outcome }
}
}
+7
View File
@@ -0,0 +1,7 @@
export default class AwEChatMessage extends ChatMessage {
/** @override */
async getHTML(...args) {
const html = await super.getHTML(...args)
return html
}
}
+6
View File
@@ -0,0 +1,6 @@
export default class AwEItem extends Item {
/** @override */
prepareData() {
super.prepareData()
}
}
+5
View File
@@ -0,0 +1,5 @@
export default class AwERoll extends Roll {
constructor(formula, data = {}, options = {}) {
super(formula, data, options)
}
}
+9
View File
@@ -0,0 +1,9 @@
export { default as AwECharacter } from "./character.mjs"
export { default as AwECreature } from "./creature.mjs"
export { default as AwEAbility } from "./ability.mjs"
export { default as AwEField } from "./field.mjs"
export { default as AwEArchetype } from "./archetype.mjs"
export { default as AwEBackground } from "./background.mjs"
export { default as AwEKit } from "./kit.mjs"
export { default as AwEWeapon } from "./weapon.mjs"
export { default as AwEEquipment } from "./equipment.mjs"
+28
View File
@@ -0,0 +1,28 @@
import { SYSTEM } from "../config/system.mjs"
export default class AwEAbility extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.abilityType = new fields.StringField({
required: true,
nullable: false,
initial: "field",
choices: Object.keys(SYSTEM.ABILITY_TYPE)
})
schema.cost = new fields.StringField({
required: true,
nullable: false,
initial: "one",
choices: Object.keys(SYSTEM.ABILITY_COST)
})
schema.frequency = new fields.StringField({ initial: "", required: false, nullable: true })
schema.requirements = new fields.StringField({ initial: "", required: false, nullable: true })
schema.trigger = new fields.StringField({ initial: "", required: false, nullable: true })
schema.traits = new fields.ArrayField(new fields.StringField())
return schema
}
}
+13
View File
@@ -0,0 +1,13 @@
export default class AwEArchetype extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.prerequisiteLevel = new fields.NumberField({
required: true, nullable: false, integer: true, initial: 2, min: 1
})
return schema
}
}
+11
View File
@@ -0,0 +1,11 @@
export default class AwEBackground extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.bonus = new fields.StringField({ initial: "", required: false, nullable: true })
return schema
}
}
+74
View File
@@ -0,0 +1,74 @@
import { SYSTEM } from "../config/system.mjs"
export default class AwECharacter extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
// Identity
schema.pronouns = new fields.StringField({ initial: "", required: false, nullable: true })
schema.fieldName = new fields.StringField({ initial: "", required: false, nullable: true })
schema.specialization = new fields.StringField({ initial: "", required: false, nullable: true })
schema.archetypeName = new fields.StringField({ initial: "", required: false, nullable: true })
schema.backgroundName = new fields.StringField({ initial: "", required: false, nullable: true })
// Core stats
schema.level = new fields.NumberField({ ...requiredInteger, initial: 1, min: 1, max: 10 })
schema.stride = new fields.NumberField({ ...requiredInteger, initial: 5, min: 0 })
// Hit Points
schema.hp = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }),
temp: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
})
// Flow Points
schema.flowPoints = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
temp: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
})
// Attributes: agility, fitness, awareness, influence
// boostLevel: how many boosts applied (0-4)
// mod = level + boostLevel (computed in prepareDerivedData)
// dc = 10 + mod (computed)
// bonus: manual +/- bonus
const attributeField = () => new fields.SchemaField({
boostLevel: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 4 }),
bonus: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 })
})
schema.attributes = new fields.SchemaField(
Object.values(SYSTEM.ATTRIBUTES).reduce((obj, attr) => {
obj[attr.id] = attributeField()
return obj
}, {})
)
return schema
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
const level = this.level
for (const attrId of Object.keys(SYSTEM.ATTRIBUTES)) {
const attr = this.attributes[attrId]
attr.mod = level + attr.boostLevel + attr.bonus
attr.dc = 10 + attr.mod
}
// Compute max HP if not overridden
// Base HP = 10 + (level * 2) + fitness modifier
const fitnessMod = this.attributes.fitness.mod ?? 0
if (this.hp.max === 10) {
this.hp.max = 10 + (level * 2) + fitnessMod
}
}
}
+46
View File
@@ -0,0 +1,46 @@
export default class AwECreature extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.level = new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 })
schema.stride = new fields.NumberField({ ...requiredInteger, initial: 5, min: 0 })
schema.hp = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 })
})
// Raw attribute values (mod = value directly, no boosts)
schema.attributes = new fields.SchemaField({
agility: new fields.SchemaField({
mod: new fields.NumberField({ ...requiredInteger, initial: 2 }),
dc: new fields.NumberField({ ...requiredInteger, initial: 12 })
}),
fitness: new fields.SchemaField({
mod: new fields.NumberField({ ...requiredInteger, initial: 2 }),
dc: new fields.NumberField({ ...requiredInteger, initial: 12 })
}),
awareness: new fields.SchemaField({
mod: new fields.NumberField({ ...requiredInteger, initial: 2 }),
dc: new fields.NumberField({ ...requiredInteger, initial: 12 })
}),
influence: new fields.SchemaField({
mod: new fields.NumberField({ ...requiredInteger, initial: 2 }),
dc: new fields.NumberField({ ...requiredInteger, initial: 12 })
})
})
// Eureka Rubric
schema.eurekaClaims = new fields.StringField({ initial: "", required: false, nullable: true })
schema.eurekaEvidence = new fields.StringField({ initial: "", required: false, nullable: true })
schema.eurekaThreshold1 = new fields.StringField({ initial: "", required: false, nullable: true })
schema.eurekaThreshold2 = new fields.StringField({ initial: "", required: false, nullable: true })
schema.eurekaHints = new fields.StringField({ initial: "", required: false, nullable: true })
return schema
}
}
+13
View File
@@ -0,0 +1,13 @@
export default class AwEEquipment extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.quantity = new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 })
schema.weight = new fields.NumberField({ required: true, nullable: false, initial: 0, min: 0 })
return schema
}
}
+26
View File
@@ -0,0 +1,26 @@
import { SYSTEM } from "../config/system.mjs"
export default class AwEField extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.keyAttribute = new fields.StringField({
required: true,
nullable: false,
initial: "agility",
choices: Object.keys(SYSTEM.ATTRIBUTES)
})
schema.keyAttribute2 = new fields.StringField({
required: false,
nullable: true,
initial: null,
choices: [...Object.keys(SYSTEM.ATTRIBUTES), null]
})
schema.specializations = new fields.ArrayField(new fields.StringField())
schema.knowledgeBonus = new fields.StringField({ initial: "", required: false, nullable: true })
return schema
}
}
+16
View File
@@ -0,0 +1,16 @@
export default class AwEKit extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.fieldName = new fields.StringField({ initial: "", required: false, nullable: true })
schema.charges = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
})
return schema
}
}
+23
View File
@@ -0,0 +1,23 @@
import { SYSTEM } from "../config/system.mjs"
export default class AwEWeapon extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.range = new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 })
schema.damageFormula = new fields.StringField({ initial: "1d6", required: true, nullable: false })
schema.damageType = new fields.StringField({ initial: "physical", required: true, nullable: false })
schema.attackAttribute = new fields.StringField({
required: true,
nullable: false,
initial: "agility",
choices: Object.keys(SYSTEM.ATTRIBUTES)
})
schema.traits = new fields.ArrayField(new fields.StringField())
return schema
}
}
+3996
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "fvtt-adventures-with-emmy",
"private": true,
"version": "1.0.0",
"devDependencies": {
"@eslint/js": "^9.8.0",
"@foundryvtt/foundryvtt-cli": "^1.0.2",
"commander": "^11.1.0",
"eslint": "^9.9.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsdoc": "^48.11.0",
"eslint-plugin-prettier": "^5.2.1",
"globals": "^15.9.0",
"less": "^4.1.3",
"prettier": "^3.3.3"
},
"author": "LeRatierBretonnien",
"license": "MIT",
"dependencies": {
"gulp": "^5.0.0",
"gulp-less": "^5.0.0"
},
"scripts": {
"packPacks": "node ./tools/packPacks.mjs",
"unpackPacks": "node ./tools/unpackPacks.mjs"
},
"description": "Adventures with Emmy - STEM Education RPG for Foundry Virtual TableTop",
"main": "gulpfile.js",
"repository": {
"type": "git",
"url": "https://www.uberwald.me/gitea/uberwald/fvtt-adventures-with-emmy.git"
}
}
View File
+455
View File
@@ -0,0 +1,455 @@
// Adventures with Emmy - Main LESS stylesheet
// ============================================================
// Variables
// ============================================================
@color-primary: #3a5a8c;
@color-secondary: #e8a020;
@color-accent: #2ecc71;
@color-bg: #f5f0e8;
@color-text: #2c2c2c;
@color-text-light: #6c6c6c;
@color-border: #c8b89a;
@color-success: #27ae60;
@color-failure: #e74c3c;
@color-critical-success: #1abc9c;
@color-critical-failure: #8e44ad;
@font-heading: "Signika", "Palatino Linotype", serif;
@font-body: "Signika", "Book Antiqua", serif;
// ============================================================
// Base Styles
// ============================================================
.awemmy {
font-family: @font-body;
color: @color-text;
background: @color-bg;
.window-header {
background: @color-primary;
color: white;
.window-title {
font-family: @font-heading;
font-size: 1.1rem;
}
}
// --------------------------------------------------------
// Sheet Controls
// --------------------------------------------------------
.sheet-controls {
display: flex;
gap: 0.25rem;
align-items: center;
.toggle-sheet {
cursor: pointer;
padding: 0.25rem 0.5rem;
border: 1px solid @color-border;
border-radius: 3px;
background: white;
&:hover {
background: @color-secondary;
color: white;
border-color: @color-secondary;
}
}
}
// --------------------------------------------------------
// Actor Header
// --------------------------------------------------------
.actor-header {
display: flex;
gap: 1rem;
padding: 0.5rem;
background: lighten(@color-primary, 50%);
border-bottom: 2px solid @color-border;
.actor-img {
width: 80px;
height: 80px;
object-fit: cover;
border: 2px solid @color-border;
border-radius: 4px;
cursor: pointer;
&:hover {
border-color: @color-secondary;
}
}
.actor-name {
font-family: @font-heading;
font-size: 1.4rem;
font-weight: bold;
color: @color-primary;
}
}
// --------------------------------------------------------
// Attributes Table
// --------------------------------------------------------
.attributes-table {
width: 100%;
border-collapse: collapse;
margin: 0.5rem 0;
thead tr {
background: @color-primary;
color: white;
th {
padding: 0.4rem 0.5rem;
text-align: center;
font-size: 0.8rem;
text-transform: uppercase;
}
}
tbody tr {
&:nth-child(even) {
background: lighten(@color-primary, 55%);
}
td {
padding: 0.35rem 0.5rem;
text-align: center;
border-bottom: 1px solid @color-border;
input {
width: 50px;
text-align: center;
}
}
.attr-label {
text-align: left;
font-weight: bold;
}
.rollable {
cursor: pointer;
color: @color-primary;
&:hover {
color: @color-secondary;
text-decoration: underline;
}
}
}
}
// --------------------------------------------------------
// HP / Flow Points
// --------------------------------------------------------
.resource-block {
display: flex;
align-items: center;
gap: 0.5rem;
label {
font-size: 0.8rem;
text-transform: uppercase;
color: @color-text-light;
min-width: 80px;
}
.resource-values {
display: flex;
align-items: center;
gap: 0.25rem;
input {
width: 45px;
text-align: center;
}
.separator {
color: @color-text-light;
}
}
.resource-stepper {
display: flex;
gap: 0.15rem;
button {
width: 22px;
height: 22px;
padding: 0;
font-size: 0.9rem;
line-height: 1;
cursor: pointer;
border: 1px solid @color-border;
border-radius: 3px;
background: white;
&:hover {
background: @color-secondary;
color: white;
}
}
}
}
// --------------------------------------------------------
// Item Lists
// --------------------------------------------------------
.item-list {
.item-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.3rem 0.25rem;
border-bottom: 1px solid lighten(@color-border, 10%);
&:hover {
background: lighten(@color-primary, 55%);
}
.item-img {
width: 28px;
height: 28px;
object-fit: cover;
border-radius: 3px;
border: 1px solid @color-border;
}
.item-name {
flex: 1;
font-weight: bold;
}
.item-controls {
display: flex;
gap: 0.25rem;
a {
color: @color-text-light;
cursor: pointer;
&:hover {
color: @color-primary;
}
&[data-action="delete"]:hover {
color: @color-failure;
}
}
}
}
.item-add {
margin-top: 0.25rem;
button {
width: 100%;
cursor: pointer;
border: 1px dashed @color-border;
background: transparent;
padding: 0.25rem;
color: @color-text-light;
&:hover {
color: @color-primary;
border-color: @color-primary;
background: lighten(@color-primary, 55%);
}
}
}
}
// --------------------------------------------------------
// Fieldsets
// --------------------------------------------------------
fieldset {
border: 1px solid @color-border;
border-radius: 4px;
padding: 0.5rem;
margin: 0.5rem 0;
legend {
font-family: @font-heading;
font-weight: bold;
color: @color-primary;
padding: 0 0.5rem;
}
}
// --------------------------------------------------------
// Tab Navigation
// --------------------------------------------------------
.tabs {
display: flex;
gap: 0;
border-bottom: 2px solid @color-primary;
.item {
padding: 0.4rem 1rem;
cursor: pointer;
border: 1px solid transparent;
border-bottom: none;
color: @color-text-light;
&:hover {
color: @color-primary;
background: lighten(@color-primary, 55%);
}
&.active {
background: white;
color: @color-primary;
border-color: @color-primary;
border-bottom-color: white;
font-weight: bold;
margin-bottom: -2px;
}
}
}
// --------------------------------------------------------
// Creature Sheet - Eureka Rubric
// --------------------------------------------------------
.eureka-rubric {
background: lighten(@color-accent, 45%);
border: 1px solid @color-accent;
border-radius: 4px;
padding: 0.5rem;
.eureka-row {
display: flex;
align-items: flex-start;
gap: 0.5rem;
margin-bottom: 0.4rem;
label {
font-weight: bold;
min-width: 80px;
color: darken(@color-accent, 15%);
}
input, textarea {
flex: 1;
}
}
}
}
// ============================================================
// Item Sheets
// ============================================================
.awemmy.item {
.item-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem;
background: lighten(@color-primary, 50%);
border-bottom: 2px solid @color-border;
.item-img {
width: 56px;
height: 56px;
object-fit: cover;
border: 2px solid @color-border;
border-radius: 4px;
cursor: pointer;
&:hover {
border-color: @color-secondary;
}
}
input[type="text"] {
font-family: @font-heading;
font-size: 1.2rem;
font-weight: bold;
flex: 1;
border: none;
background: transparent;
&:focus {
background: white;
border: 1px solid @color-border;
}
}
}
.item-body {
padding: 0.5rem;
.form-group {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.4rem;
label {
min-width: 120px;
font-size: 0.85rem;
color: @color-text-light;
text-transform: uppercase;
}
}
}
}
// ============================================================
// Chat Message
// ============================================================
.awemmy-chat {
padding: 0.5rem;
.chat-roll-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
.chat-actor-img {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid @color-border;
}
.chat-roll-label {
font-family: @font-heading;
font-size: 1rem;
font-weight: bold;
color: @color-primary;
}
}
.roll-result {
text-align: center;
font-size: 1.6rem;
font-weight: bold;
padding: 0.5rem;
border-radius: 4px;
margin: 0.5rem 0;
background: lighten(@color-primary, 50%);
color: @color-primary;
}
.outcome-badge {
text-align: center;
padding: 0.3rem 0.75rem;
border-radius: 12px;
font-weight: bold;
font-size: 0.9rem;
&.critical-success { background: @color-critical-success; color: white; }
&.success { background: @color-success; color: white; }
&.failure { background: @color-failure; color: white; }
&.critical-failure { background: @color-critical-failure; color: white; }
}
}
+38
View File
@@ -0,0 +1,38 @@
{
"id": "fvtt-adventures-with-emmy",
"title": "Adventures with Emmy",
"description": "Adventures with Emmy - STEM Education RPG",
"manifest": "#{MANIFEST}#",
"download": "#{DOWNLOAD}#",
"url": "#{URL}#",
"license": "LICENSE",
"version": "13.0.0",
"authors": [{ "name": "Uberwald", "discord": "LeRatierBretonnien" }],
"compatibility": { "minimum": "13", "verified": "13" },
"esmodules": ["adventures-with-emmy.mjs"],
"styles": ["css/adventures-with-emmy.css"],
"languages": [{ "lang": "en", "name": "English", "path": "lang/en.json" }],
"documentTypes": {
"Actor": {
"character": { "htmlFields": ["description", "notes"] },
"creature": { "htmlFields": ["description"] }
},
"Item": {
"ability": { "htmlFields": ["description"] },
"field": { "htmlFields": ["description"] },
"archetype": { "htmlFields": ["description"] },
"background": { "htmlFields": ["description"] },
"kit": { "htmlFields": ["description"] },
"weapon": { "htmlFields": ["description"] },
"equipment": { "htmlFields": ["description"] }
}
},
"grid": { "distance": 1, "units": "m" },
"primaryTokenAttribute": "hp",
"flags": {
"hotReload": {
"extensions": ["css", "hbs", "json"],
"paths": ["css/", "lang/", "assets/", "templates/"]
}
}
}
+38
View File
@@ -0,0 +1,38 @@
<section>
<div class="item-header">
<img class="item-img" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<div class="item-body">
<div class="form-group">
<label>Type</label>
{{formField systemFields.abilityType value=system.abilityType localize=true}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Ability.Cost"}}</label>
{{formField systemFields.cost value=system.cost}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Ability.Frequency"}}</label>
{{formInput systemFields.frequency value=system.frequency}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Ability.Requirements"}}</label>
{{formInput systemFields.requirements value=system.requirements}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Ability.Trigger"}}</label>
{{formInput systemFields.trigger value=system.trigger}}
</div>
</div>
<fieldset>
<legend>Description</legend>
{{formInput
systemFields.description
enriched=enrichedDescription
value=system.description
name="system.description"
toggled=true
}}
</fieldset>
</section>
+22
View File
@@ -0,0 +1,22 @@
<section>
<div class="item-header">
<img class="item-img" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<div class="item-body">
<div class="form-group">
<label>Prerequisite Level</label>
{{formInput systemFields.prerequisiteLevel value=system.prerequisiteLevel}}
</div>
</div>
<fieldset>
<legend>Description</legend>
{{formInput
systemFields.description
enriched=enrichedDescription
value=system.description
name="system.description"
toggled=true
}}
</fieldset>
</section>
+22
View File
@@ -0,0 +1,22 @@
<section>
<div class="item-header">
<img class="item-img" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<div class="item-body">
<div class="form-group">
<label>{{localize "AWEMMY.Background.Bonus"}}</label>
{{formInput systemFields.bonus value=system.bonus}}
</div>
</div>
<fieldset>
<legend>Description</legend>
{{formInput
systemFields.description
enriched=enrichedDescription
value=system.description
name="system.description"
toggled=true
}}
</fieldset>
</section>
+49
View File
@@ -0,0 +1,49 @@
<section class="tab character-biography {{tab.cssClass}}" data-tab="{{tab.id}}" data-group="{{tab.group}}">
<fieldset>
<legend>Identity</legend>
<div class="form-group">
<label>Pronouns</label>
{{formInput systemFields.pronouns value=system.pronouns disabled=isPlayMode}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Kit.Field"}}</label>
{{formInput systemFields.fieldName value=system.fieldName disabled=isPlayMode}}
</div>
<div class="form-group">
<label>Specialization</label>
{{formInput systemFields.specialization value=system.specialization disabled=isPlayMode}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Item.Archetype"}}</label>
{{formInput systemFields.archetypeName value=system.archetypeName disabled=isPlayMode}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Item.Background"}}</label>
{{formInput systemFields.backgroundName value=system.backgroundName disabled=isPlayMode}}
</div>
</fieldset>
<fieldset>
<legend>Description</legend>
{{formInput
systemFields.description
enriched=enrichedDescription
value=system.description
name="system.description"
toggled=true
}}
</fieldset>
<fieldset>
<legend>Notes</legend>
{{formInput
systemFields.notes
enriched=enrichedNotes
value=system.notes
name="system.notes"
toggled=true
}}
</fieldset>
</section>
+79
View File
@@ -0,0 +1,79 @@
<section class="tab character-equipment {{tab.cssClass}}" data-tab="{{tab.id}}" data-group="{{tab.group}}">
{{!-- Kits --}}
<fieldset>
<legend>{{localize "AWEMMY.Item.Kit"}}</legend>
<div class="item-list">
{{#each kits as |item|}}
<div class="item-row" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<div class="item-name">{{item.name}}</div>
<div class="item-charges">{{item.system.charges.value}}/{{item.system.charges.max}}</div>
<div class="item-controls">
<a data-action="edit" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
{{#if ../isEditMode}}
<a data-action="delete" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
{{/if}}
</div>
</div>
{{/each}}
{{#if isEditMode}}
<div class="item-add">
<button type="button" data-action="createKit"><i class="fas fa-plus"></i> {{localize "AWEMMY.Item.Kit"}}</button>
</div>
{{/if}}
</div>
</fieldset>
{{!-- Weapons --}}
<fieldset>
<legend>{{localize "AWEMMY.Item.Weapon"}}</legend>
<div class="item-list">
{{#each weapons as |item|}}
<div class="item-row" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<div class="item-name">{{item.name}}</div>
<div class="item-damage">{{item.system.damageFormula}} ({{item.system.damageType}})</div>
<div class="item-range">Range: {{item.system.range}}</div>
<div class="item-controls">
<a data-action="edit" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
{{#if ../isEditMode}}
<a data-action="delete" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
{{/if}}
</div>
</div>
{{/each}}
{{#if isEditMode}}
<div class="item-add">
<button type="button" data-action="createWeapon"><i class="fas fa-plus"></i> {{localize "AWEMMY.Item.Weapon"}}</button>
</div>
{{/if}}
</div>
</fieldset>
{{!-- Equipment --}}
<fieldset>
<legend>{{localize "AWEMMY.Item.Equipment"}}</legend>
<div class="item-list">
{{#each equipments as |item|}}
<div class="item-row" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<div class="item-name">{{item.name}}</div>
<div class="item-qty">x{{item.system.quantity}}</div>
<div class="item-controls">
<a data-action="edit" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
{{#if ../isEditMode}}
<a data-action="delete" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
{{/if}}
</div>
</div>
{{/each}}
{{#if isEditMode}}
<div class="item-add">
<button type="button" data-action="createEquipment"><i class="fas fa-plus"></i> {{localize "AWEMMY.Item.Equipment"}}</button>
</div>
{{/if}}
</div>
</fieldset>
</section>
+119
View File
@@ -0,0 +1,119 @@
<section class="character-main character-main-{{#if isPlayMode}}play{{else}}edit{{/if}}">
{{!-- Header: image + name + basic stats --}}
<div class="actor-header">
<img class="actor-img" src="{{actor.img}}" data-edit="img" data-action="editImage" data-tooltip="{{actor.name}}" />
<div class="actor-identity">
{{formInput fields.name value=source.name classes="actor-name"}}
<div class="actor-details">
<div class="detail-item">
<label>{{localize "AWEMMY.Character.Level"}}</label>
{{formInput systemFields.level value=system.level disabled=isPlayMode}}
</div>
<div class="detail-item">
<label>{{localize "AWEMMY.Character.Stride"}}</label>
{{formInput systemFields.stride value=system.stride disabled=isPlayMode}}
</div>
</div>
</div>
<div class="actor-stats">
{{!-- HP --}}
<div class="resource-block">
<label>{{localize "AWEMMY.Character.HP"}}</label>
<div class="resource-values">
{{formInput systemFields.hp.fields.value value=system.hp.value classes="hp-value"}}
<span class="separator">/</span>
{{formInput systemFields.hp.fields.max value=system.hp.max disabled=isPlayMode classes="hp-max"}}
</div>
</div>
{{!-- Flow Points --}}
<div class="resource-block">
<label>{{localize "AWEMMY.Character.FlowPoints"}}</label>
<div class="resource-values">
{{formInput systemFields.flowPoints.fields.value value=system.flowPoints.value classes="fp-value"}}
</div>
{{#if isPlayMode}}
<div class="resource-stepper">
<button type="button" data-action="flowPointsPlus" data-tooltip="+1">+</button>
<button type="button" data-action="flowPointsMinus" data-tooltip="-1"></button>
</div>
{{/if}}
</div>
</div>
<div class="sheet-controls">
<button type="button" data-action="toggleSheet" data-tooltip="{{#if isPlayMode}}{{localize 'AWEMMY.Sheet.EditMode'}}{{else}}{{localize 'AWEMMY.Sheet.PlayMode'}}{{/if}}">
{{#if isPlayMode}}<i class="fa-solid fa-lock"></i>{{else}}<i class="fa-solid fa-unlock"></i>{{/if}}
</button>
</div>
</div>
{{!-- Attributes Table --}}
<fieldset>
<legend>Attributes</legend>
<table class="attributes-table">
<thead>
<tr>
<th>Attribute</th>
<th>{{localize "AWEMMY.Character.BoostLevel"}}</th>
<th>{{localize "AWEMMY.Character.Mod"}}</th>
<th>{{localize "AWEMMY.Character.DC"}}</th>
</tr>
</thead>
<tbody>
<tr>
<td class="attr-label rollable" data-attribute-id="agility">{{localize "AWEMMY.Attribute.Agility"}} <i class="fa-solid fa-dice-d20"></i></td>
<td>{{formInput systemFields.attributes.fields.agility.fields.boostLevel value=system.attributes.agility.boostLevel disabled=isPlayMode}}</td>
<td>{{system.attributes.agility.mod}}</td>
<td>{{system.attributes.agility.dc}}</td>
</tr>
<tr>
<td class="attr-label rollable" data-attribute-id="fitness">{{localize "AWEMMY.Attribute.Fitness"}} <i class="fa-solid fa-dice-d20"></i></td>
<td>{{formInput systemFields.attributes.fields.fitness.fields.boostLevel value=system.attributes.fitness.boostLevel disabled=isPlayMode}}</td>
<td>{{system.attributes.fitness.mod}}</td>
<td>{{system.attributes.fitness.dc}}</td>
</tr>
<tr>
<td class="attr-label rollable" data-attribute-id="awareness">{{localize "AWEMMY.Attribute.Awareness"}} <i class="fa-solid fa-dice-d20"></i></td>
<td>{{formInput systemFields.attributes.fields.awareness.fields.boostLevel value=system.attributes.awareness.boostLevel disabled=isPlayMode}}</td>
<td>{{system.attributes.awareness.mod}}</td>
<td>{{system.attributes.awareness.dc}}</td>
</tr>
<tr>
<td class="attr-label rollable" data-attribute-id="influence">{{localize "AWEMMY.Attribute.Influence"}} <i class="fa-solid fa-dice-d20"></i></td>
<td>{{formInput systemFields.attributes.fields.influence.fields.boostLevel value=system.attributes.influence.boostLevel disabled=isPlayMode}}</td>
<td>{{system.attributes.influence.mod}}</td>
<td>{{system.attributes.influence.dc}}</td>
</tr>
</tbody>
</table>
</fieldset>
{{!-- Abilities --}}
<fieldset>
<legend>{{localize "AWEMMY.Item.Ability"}}</legend>
<div class="item-list">
{{#each abilities as |item|}}
<div class="item-row" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<div class="item-name">{{item.name}}</div>
<div class="item-cost">{{item.system.cost}}</div>
<div class="item-controls">
<a data-action="edit" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-tooltip="Edit"><i class="fas fa-edit"></i></a>
{{#if ../isEditMode}}
<a data-action="delete" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-tooltip="Delete"><i class="fas fa-trash"></i></a>
{{/if}}
</div>
</div>
{{/each}}
{{#if isEditMode}}
<div class="item-add">
<button type="button" data-action="createAbility"><i class="fas fa-plus"></i> {{localize "AWEMMY.Item.Ability"}}</button>
</div>
{{/if}}
</div>
</fieldset>
</section>
+27
View File
@@ -0,0 +1,27 @@
<div class="awemmy-chat">
<div class="chat-roll-header">
<div class="chat-roll-label">{{flavor}}</div>
</div>
{{#unless isPrivate}}
<div class="roll-result">{{total}}</div>
{{#if outcome}}
<div class="outcome-badge {{outcome}}">
{{#if (eq outcome "criticalSuccess")}}
<i class="fa-solid fa-star"></i> {{localize "AWEMMY.Roll.CriticalSuccess"}}
{{else if (eq outcome "success")}}
<i class="fa-solid fa-circle-check"></i> {{localize "AWEMMY.Roll.Success"}}
{{else if (eq outcome "failure")}}
<i class="fa-solid fa-circle-xmark"></i> {{localize "AWEMMY.Roll.Failure"}}
{{else if (eq outcome "criticalFailure")}}
<i class="fa-solid fa-skull"></i> {{localize "AWEMMY.Roll.CriticalFailure"}}
{{/if}}
</div>
{{/if}}
{{else}}
<div class="private-result">
<i class="fa-solid fa-eye-slash"></i> Private Roll
</div>
{{/unless}}
</div>
+106
View File
@@ -0,0 +1,106 @@
<section class="creature-main">
<div class="actor-header">
<img class="actor-img" src="{{actor.img}}" data-edit="img" data-action="editImage" data-tooltip="{{actor.name}}" />
<div class="actor-identity">
{{formInput fields.name value=source.name classes="actor-name"}}
<div class="actor-details">
<div class="detail-item">
<label>{{localize "AWEMMY.Character.Level"}}</label>
{{formInput systemFields.level value=system.level}}
</div>
<div class="detail-item">
<label>{{localize "AWEMMY.Character.Stride"}}</label>
{{formInput systemFields.stride value=system.stride}}
</div>
<div class="detail-item">
<label>{{localize "AWEMMY.Character.HP"}}</label>
{{formInput systemFields.hp.fields.value value=system.hp.value}}
<span>/</span>
{{formInput systemFields.hp.fields.max value=system.hp.max}}
</div>
</div>
</div>
<div class="sheet-controls">
<button type="button" data-action="toggleSheet">
{{#if isPlayMode}}<i class="fa-solid fa-lock"></i>{{else}}<i class="fa-solid fa-unlock"></i>{{/if}}
</button>
</div>
</div>
{{!-- Attributes --}}
<fieldset>
<legend>Attributes</legend>
<table class="attributes-table">
<thead>
<tr>
<th>Attribute</th>
<th>{{localize "AWEMMY.Character.Mod"}}</th>
<th>{{localize "AWEMMY.Character.DC"}}</th>
</tr>
</thead>
<tbody>
<tr>
<td class="attr-label">{{localize "AWEMMY.Attribute.Agility"}}</td>
<td>{{formInput systemFields.attributes.fields.agility.fields.mod value=system.attributes.agility.mod}}</td>
<td>{{formInput systemFields.attributes.fields.agility.fields.dc value=system.attributes.agility.dc}}</td>
</tr>
<tr>
<td class="attr-label">{{localize "AWEMMY.Attribute.Fitness"}}</td>
<td>{{formInput systemFields.attributes.fields.fitness.fields.mod value=system.attributes.fitness.mod}}</td>
<td>{{formInput systemFields.attributes.fields.fitness.fields.dc value=system.attributes.fitness.dc}}</td>
</tr>
<tr>
<td class="attr-label">{{localize "AWEMMY.Attribute.Awareness"}}</td>
<td>{{formInput systemFields.attributes.fields.awareness.fields.mod value=system.attributes.awareness.mod}}</td>
<td>{{formInput systemFields.attributes.fields.awareness.fields.dc value=system.attributes.awareness.dc}}</td>
</tr>
<tr>
<td class="attr-label">{{localize "AWEMMY.Attribute.Influence"}}</td>
<td>{{formInput systemFields.attributes.fields.influence.fields.mod value=system.attributes.influence.mod}}</td>
<td>{{formInput systemFields.attributes.fields.influence.fields.dc value=system.attributes.influence.dc}}</td>
</tr>
</tbody>
</table>
</fieldset>
{{!-- Description --}}
<fieldset>
<legend>Description</legend>
{{formInput
systemFields.description
enriched=enrichedDescription
value=system.description
name="system.description"
toggled=true
}}
</fieldset>
{{!-- Eureka Rubric --}}
<fieldset>
<legend>{{localize "AWEMMY.Creature.EurekaRubric"}}</legend>
<div class="eureka-rubric">
<div class="eureka-row">
<label>{{localize "AWEMMY.Creature.Claims"}}</label>
{{formInput systemFields.eurekaClaims value=system.eurekaClaims}}
</div>
<div class="eureka-row">
<label>{{localize "AWEMMY.Creature.Evidence"}}</label>
{{formInput systemFields.eurekaEvidence value=system.eurekaEvidence}}
</div>
<div class="eureka-row">
<label>Threshold 1</label>
{{formInput systemFields.eurekaThreshold1 value=system.eurekaThreshold1}}
</div>
<div class="eureka-row">
<label>Threshold 2</label>
{{formInput systemFields.eurekaThreshold2 value=system.eurekaThreshold2}}
</div>
<div class="eureka-row">
<label>{{localize "AWEMMY.Creature.Hints"}}</label>
{{formInput systemFields.eurekaHints value=system.eurekaHints}}
</div>
</div>
</fieldset>
</section>
+26
View File
@@ -0,0 +1,26 @@
<section>
<div class="item-header">
<img class="item-img" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<div class="item-body">
<div class="form-group">
<label>{{localize "AWEMMY.Equipment.Quantity"}}</label>
{{formInput systemFields.quantity value=system.quantity}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Equipment.Weight"}}</label>
{{formInput systemFields.weight value=system.weight}}
</div>
</div>
<fieldset>
<legend>Description</legend>
{{formInput
systemFields.description
enriched=enrichedDescription
value=system.description
name="system.description"
toggled=true
}}
</fieldset>
</section>
+26
View File
@@ -0,0 +1,26 @@
<section>
<div class="item-header">
<img class="item-img" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<div class="item-body">
<div class="form-group">
<label>{{localize "AWEMMY.Field.KeyAttribute"}}</label>
{{formField systemFields.keyAttribute value=system.keyAttribute localize=true}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Field.KnowledgeBonus"}}</label>
{{formInput systemFields.knowledgeBonus value=system.knowledgeBonus}}
</div>
</div>
<fieldset>
<legend>Description</legend>
{{formInput
systemFields.description
enriched=enrichedDescription
value=system.description
name="system.description"
toggled=true
}}
</fieldset>
</section>
+28
View File
@@ -0,0 +1,28 @@
<section>
<div class="item-header">
<img class="item-img" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<div class="item-body">
<div class="form-group">
<label>{{localize "AWEMMY.Kit.Field"}}</label>
{{formInput systemFields.fieldName value=system.fieldName}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Kit.Charges"}}</label>
{{formInput systemFields.charges.fields.value value=system.charges.value}}
<span>/</span>
{{formInput systemFields.charges.fields.max value=system.charges.max}}
</div>
</div>
<fieldset>
<legend>Description</legend>
{{formInput
systemFields.description
enriched=enrichedDescription
value=system.description
name="system.description"
toggled=true
}}
</fieldset>
</section>
+34
View File
@@ -0,0 +1,34 @@
<section>
<div class="item-header">
<img class="item-img" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<div class="item-body">
<div class="form-group">
<label>{{localize "AWEMMY.Weapon.AttackAttribute"}}</label>
{{formField systemFields.attackAttribute value=system.attackAttribute localize=true}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Weapon.Damage"}}</label>
{{formInput systemFields.damageFormula value=system.damageFormula}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Weapon.DamageType"}}</label>
{{formInput systemFields.damageType value=system.damageType}}
</div>
<div class="form-group">
<label>{{localize "AWEMMY.Weapon.Range"}}</label>
{{formInput systemFields.range value=system.range}}
</div>
</div>
<fieldset>
<legend>Description</legend>
{{formInput
systemFields.description
enriched=enrichedDescription
value=system.description
name="system.description"
toggled=true
}}
</fieldset>
</section>