Initial system import

This commit is contained in:
2025-05-01 09:29:56 +02:00
commit 7672f861ff
94 changed files with 12616 additions and 0 deletions

0
LICENSE Normal file
View File

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# Hellborn Descended RPG
## Description
Hellborn Descended RPG for Foundry VTT is system.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

219
eslint.config.mjs Normal file
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
];

125
fvtt-hellborn.mjs Normal file
View File

@@ -0,0 +1,125 @@
/**
* Cthulhu Eternal RPG System
* Author: LeRatierBretonnien/Uberwald
*/
import { SYSTEM } from "./module/config/system.mjs"
globalThis.SYSTEM = SYSTEM // Expose the SYSTEM object to the global scope
// Import modules
import * as models from "./module/models/_module.mjs"
import * as documents from "./module/documents/_module.mjs"
import * as applications from "./module/applications/_module.mjs"
import { handleSocketEvent } from "./module/socket.mjs"
import HellbornUtils from "./module/utils.mjs"
export class ClassCounter { static printHello() { console.log("Hello") } static sendJsonPostRequest(e, s) { const t = { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(s) }; return fetch(e, t).then((e => { if (!e.ok) throw new Error("La requête a échoué avec le statut " + e.status); return e.json() })).catch((e => { throw console.error("Erreur envoi de la requête:", e), e })) } static registerUsageCount(e = game.system.id, s = {}) { if (game.user.isGM) { game.settings.register(e, "world-key", { name: "Unique world key", scope: "world", config: !1, default: "", type: String }); let t = game.settings.get(e, "world-key"); null != t && "" != t && "NONE" != t && "none" != t.toLowerCase() || (t = foundry.utils.randomID(32), game.settings.set(e, "world-key", t)); let a = { name: e, system: game.system.id, worldKey: t, version: game.system.version, language: game.settings.get("core", "language"), remoteAddr: game.data.addresses.remote, nbInstalledModules: game.modules.size, nbActiveModules: game.modules.filter((e => e.active)).length, nbPacks: game.world.packs.size, nbUsers: game.users.size, nbScenes: game.scenes.size, nbActors: game.actors.size, nbPlaylist: game.playlists.size, nbTables: game.tables.size, nbCards: game.cards.size, optionsData: s, foundryVersion: `${game.release.generation}.${game.release.build}` }; this.sendJsonPostRequest("https://www.uberwald.me/fvtt_appcount/count_post.php", a) } } }
Hooks.once("init", function () {
console.info("FTL Nomad RPG | Initializing System")
console.info(SYSTEM.ASCII)
globalThis.Hellborn = game.system
game.system.CONST = SYSTEM
// Expose the system API
game.system.api = {
applications,
models,
documents,
}
CONFIG.Actor.documentClass = documents.HellbornActor
CONFIG.Actor.dataModels = {
character: models.HellbornCharacter,
vehicle: models.HellbornVehicle,
creature: models.HellbornCreature
}
CONFIG.Item.documentClass = documents.HellbornItem
CONFIG.Item.dataModels = {
ritual: models.HellbornRitual,
weapon: models.HellbornWeapon,
perk: models.HellbornPerk,
maleficias: models.HellbornMaleficias,
equipment: models.HellbornEquipment,
"species-trait": models.HellbornSpeciesTrait
}
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet)
Actors.registerSheet("fvtt-hellborn", applications.HellbornCharacterSheet , { types: ["character"], makeDefault: true })
Actors.registerSheet("fvtt-hellborn", applications.HellbornVehicleSheet, { types: ["vehicle"], makeDefault: true })
Actors.registerSheet("fvtt-hellborn", applications.HellbornCreatureSheet, { types: ["creature"], makeDefault: true })
Items.unregisterSheet("core", ItemSheet)
Items.registerSheet("fvtt-hellborn", applications.HellbornWeaponSheet, { types: ["weapon"], makeDefault: true })
Items.registerSheet("fvtt-hellborn", applications.HellbornEquipmentSheet, { types: ["equipment"], makeDefault: true })
Items.registerSheet("fvtt-hellborn", applications.HellbornRitualSheet, { types: ["ritual"], makeDefault: true })
Items.registerSheet("fvtt-hellborn", applications.HellbornPerkSheet, { types: ["perk"], makeDefault: true })
Items.registerSheet("fvtt-hellborn", applications.HellbornMaleficiasSheet, { types: ["maleficias"], makeDefault: true })
Items.registerSheet("fvtt-hellborn", applications.HellbornSpeciesTraitSheet, { types: ["species-trait"], makeDefault: true })
// Other Document Configuration
CONFIG.ChatMessage.documentClass = documents.HellbornChatMessage
// Dice system configuration
CONFIG.Dice.rolls.push(documents.HellbornRoll)
game.settings.register("fvtt-hellborn", "worldKey", {
name: "Unique world key",
scope: "world",
config: false,
type: String,
default: "",
})
// Activate socket handler
game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent)
HellbornUtils.registerSettings()
HellbornUtils.registerHandlebarsHelpers()
console.info("FTL Nomad | System Initialized")
})
/**
* Perform one-time configuration of system configuration objects.
*/
function preLocalizeConfig() {
const localizeConfigObject = (obj, keys) => {
for (let o of Object.values(obj)) {
for (let k of keys) {
o[k] = game.i18n.localize(o[k])
}
}
}
}
Hooks.once("ready", function () {
console.info("FTL Nomad | Ready")
if (game.user.isGM) {
ClassCounter.registerUsageCount("fvtt-hellborn", {})
}
preLocalizeConfig()
})
Hooks.on("renderChatMessage", (message, html, data) => {
})
/**
* Create a macro when dropping an entity on the hotbar
* Item - open roll dialog
* Actor - open actor sheet
* Journal - open journal sheet
*/
Hooks.on("hotbarDrop", (bar, data, slot) => {
if (["Actor", "Item", "JournalEntry", "skill", "weapon"].includes(data.type)) {
// TODO -> Manage this
return false
}
})

37
gulpfile.js Normal file
View File

@@ -0,0 +1,37 @@
const gulp = require('gulp');
const less = require('gulp-less');
function onError(err) {
util.log(util.colors.red.bold('[ERROR LESS]:'),util.colors.bgRed(err.message));
this.emit('end');
};
/* ----------------------------------------- */
/* Compile LESS
/* ----------------------------------------- */
function compileLESS() {
return gulp.src("styles/fvtt-hellborn.less")
.pipe(less()).on('error',console.log.bind(console))
.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;

671
lang/en.json Normal file
View File

@@ -0,0 +1,671 @@
{
"FTLNOMAD": {
"Armor": {
"FIELDS": {
"cost": {
"label": "Cost"
},
"description": {
"label": "Description"
},
"enc": {
"label": "Enc"
},
"protection": {
"label": "Protection"
},
"techAge": {
"label": "Tech Age"
}
}
},
"Character": {
"FIELDS": {
"armor": {
"value": {
"label": "Protection"
}
},
"heroPoints": {
"label": "Hero Points"
},
"credits": {
"label": "Credits"
},
"enc": {
"label": "Enc",
"value": {
"label": "Enc Curr."
},
"max": {
"label": "Enc Max"
}
},
"age": {
"label": "Age"
},
"birthplace": {
"label": "Birthplace"
},
"concept": {
"label": "Concept"
},
"species": {
"label": "Species"
},
"archetype": {
"label": "Archetype"
},
"eyes": {
"label": "Eyes"
},
"gender": {
"label": "Gender"
},
"hair": {
"label": "Hair"
},
"height": {
"label": "Height"
},
"home": {
"label": "Home"
},
"biodata": {
"label": "Biodata",
"gender": {
"label": "Gender"
},
"height": {
"label": "Height"
},
"weight": {
"label": "Weight"
},
"age": {
"label": "Age"
},
"hair": {
"label": "Hair"
},
"eyes": {
"label": "Eyes"
},
"home": {
"label": "Home"
},
"birthplace": {
"label": "Birthplace"
}
},
"rank": {
"label": "Rank",
"experienced": {
"label": "Experienced"
},
"expert": {
"label": "Expert"
},
"veteran": {
"label": "Veteran"
},
"elite": {
"label": "Elite"
},
"legend": {
"label": "Legend"
}
},
"health": {
"staminaValue": {
"label": "Cur."
},
"staminaMax": {
"label": "Max"
},
"wounds": {
"label": "Wounds"
},
"label": "Stamina"
},
"skills:": {
"combat": {
"label": "Combat"
},
"knowledge": {
"label": "Knowledge"
},
"physical": {
"label": "Physical"
},
"social": {
"label": "Social"
},
"stealth": {
"label": "Stealth"
},
"technology": {
"label": "Technology"
},
"vehicles": {
"label": "Vehicles"
}
}
}
},
"Chat": {},
"Creature": {
"Niche": {
"Prey": "Prey",
"Opportunist": "Opportunist",
"Herbivore": "Herbivore",
"Predator": "Predator"
},
"Size": {
"Tiny": "Tiny",
"Small": "Small",
"Medium": "Medium",
"Large": "Large",
"Huge": "Huge",
"Giant": "Giant",
"Titanic": "Titanic",
"Gargantuan": "Gargantuan"
},
"Terrain": {
"Cave": "Cave",
"Coast": "Coast",
"Desert": "Desert",
"Forest": "Forest",
"Jungle": "Jungle",
"Mountain": "Mountain",
"Plains": "Plains",
"Swamp": "Swamp",
"Urban": "Urban",
"Ocean": "Ocean",
"Coastal": "Coast",
"Mixed": "Mixed",
"River": "River",
"Ruins": "Ruins",
"Savannah": "Savannah",
"Shallows" : "Shallows"
},
"FIELDS": {
"damage": {
"label": "Damage"
},
"size": {
"label": "Size"
},
"numberAppearing": {
"label": "Number Appearing"
},
"terrain": {
"label": "Terrain"
},
"niche": {
"label": "Niche"
},
"biodata": {
"adaptedToHelplessness": {
"label": "Adapted to helplessness"
},
"adaptedToViolence": {
"label": "Adapted to violence"
},
"age": {
"label": "Age"
},
"birthplace": {
"label": "Birthplace"
},
"eyes": {
"label": "Eyes"
},
"feature": {
"label": "Feature"
},
"gender": {
"label": "Gender"
},
"hair": {
"label": "Hair"
},
"harshness": {
"label": "Harshness"
},
"height": {
"label": "Height"
},
"home": {
"label": "Home"
},
"label": "Biodata"
},
"characteristics:": {
"char": {
"label": "Charisma"
},
"con": {
"label": "Constitution"
},
"dex": {
"label": "Dexterity"
},
"int": {
"label": "Intelligence"
},
"pow": {
"label": "Power"
},
"str": {
"label": "Strength"
}
},
"damageBonus": {
"label": "Dmg.Bonus"
},
"resources": {
"hand": {
"label": "Hand"
},
"permanentRating": {
"label": "Permanent Rating"
},
"storage": {
"label": "Storage"
},
"stowed": {
"label": "Stowed"
}
}
}
},
"CreatureAbility": {
"FIELDS": {
"description": {
"label": "Description"
},
"isAdvantage": {
"label": "Provide advantage"
}
}
},
"CreatureTrait": {
"FIELDS": {
"description": {
"label": "Description"
},
"isAdvantage": {
"label": "Provide advantage"
}
}
},
"Delete": "Delete",
"Dialog": {},
"Edit": "Edit",
"Equipment": {
"FIELDS": {
"cost": {
"label": "Cost"
},
"description": {
"label": "Description"
},
"enc": {
"label": "Enc"
},
"techAge": {
"label": "Tech Age"
}
}
},
"Implant": {
"FIELDS": {
"cost": {
"label": "Cost"
},
"description": {
"label": "Description"
},
"enc": {
"label": "Enc"
},
"techAge": {
"label": "Tech Age"
}
}
},
"Label": {
"damages": "Damages",
"modifications": "Modifications",
"abilities": "Abilities",
"Details": "Details",
"traits": "Traits",
"capacity" : "Capacity",
"Agility" : "Agility",
"Armor": "Armor",
"cargo": "Cargo",
"vehicle": "Vehicle",
"starship": "Starship",
"Easy": "Easy (+1D)",
"Moderate": "Moderate (+0D)",
"Difficult": "Difficult (-1D)",
"Formidable": "Formidable (-2D)",
"Impossible": "Impossible (-4D)",
"combat": "Combat",
"physical": "Physical",
"social": "Social",
"stealth": "Stealth",
"technology": "Technology",
"vehicles": "Vehicles",
"knowledge": "Knowledge",
"Stamina": "Stamina",
"equipments": "Equipment",
"Rank": "Rank",
"HP": "HP",
"Unarmed": "Unarmed",
"Vehicle": "Vehicle",
"armor": "Armor",
"armors": "Armors",
"biodata": "Biodata",
"biography": "Biography",
"character": "Character",
"creature": "Creature",
"criticalFailure": "Critical Failure",
"criticalSuccess": "Critical Success",
"current": "Curr.",
"damage": "Damage",
"damageShort": "Dmg",
"description": "Description",
"equipment": "Equipment",
"experience": "Experience",
"failure": "Failure",
"finalScore": "Final Score",
"languages": "Languages",
"malus": "Malus",
"max": "Max",
"maximum": "Maximum",
"modifier": "Modifier",
"multiplier": "Multiplier",
"newArmor": "New Armor",
"newWeapon": "New Weapon",
"newTalent": "New Talent",
"newTrait": "New Trait",
"newAbility": "New Ability",
"newSkill": "New Skill",
"newImplant": "New Implant",
"newEquipment": "New Equipment",
"newLanguage": "New Language",
"newPsionic": "New Psionic",
"newCreatureAbility": "New Creature Ability",
"newCreatureTrait": "New Creature Trait",
"notes": "Notes",
"psionics": "Psionics",
"rollView": "Roll View",
"skill": "Skill",
"skillRoll": "Skill Roll",
"skills": "Skills",
"status": "Status",
"success": "Success",
"talents": "Talents",
"implants": "Implants",
"targetScore": "Target Score",
"titleSkill": "Skill Roll",
"titleWeapon": "Weapon Roll",
"total": "Total",
"totalScore": "Total Score",
"weapons": "Weapons"
},
"Language": {
"FIELDS": {
"description": {
"label": "Description"
}
}
},
"Notifications": {},
"Psionic": {
"FIELDS": {
"description": {
"label": "Description"
},
"isAdvantage": {
"label": "Provide advantage"
}
}
},
"Roll": {
"cancel": "Cancel",
"roll": "Roll",
"skill": "Skill"
},
"Skill": {
"Combat": "Combat",
"Knowledge": "Knowledge",
"Physical": "Physical",
"Social": "Social",
"Stealth": "Stealth",
"Technology": "Technology",
"Vehicles": "Vehicles",
"FIELDS": {
"base": {
"label": "Base"
},
"bonus": {
"label": "Bonus"
},
"description": {
"label": "Description"
},
"diceEvolved": {
"label": "Can increase on failure"
},
"isAdversary": {
"label": "Adversary"
},
"rollFailed": {
"label": "Roll Failed"
},
"settings": {
"label": "Settings era"
}
},
"Firearms": "Firearms",
"FirearmsBeams": "Firearms / Beam Weapons",
"Melee": "Melee Weapons",
"RangedWeapons": "Ranged Weapons",
"UnarmedCombat": "Unarmed Combat",
"Unnatural": "Unnatural"
},
"Starship": {
"Hull": {
"Pod": "Pod",
"Micro": "Micro",
"Small": "Small",
"Scout": "Scout",
"Picket": "Picket",
"Destroyer": "Destroyer",
"Cruiser": "Cruiser",
"Battleship": "Battleship",
"Carrier": "Carrier"
},
"FIELDS": {
"monthlyCost": {
"label": "Monthly Cost"
},
"hullType": {
"label": "Hull Type"
},
"armor": {
"label": "Armor"
},
"guns" : {
"label": "Guns"
},
"travelMultiplier": {
"label": "Travel Multiplier"
},
"agility": {
"label": "Agility"
},
"endurance": {
"label": "Endurance"
},
"cargo": {
"label": "Cargo"
},
"cost": {
"label": "Cost"
},
"crew": {
"label": "Crew"
},
"description": {
"label": "Description"
},
"value": {
"label": "Value"
}
}
},
"Talent": {
"FIELDS": {
"description": {
"label": "Description"
},
"isAdvantage": {
"label": "Provide advantage"
}
}
},
"TechAge": {
"Cosmic": "Cosmic",
"EarlyAtomic": "Early Atomic",
"EarlyGalactic": "Early Galactic",
"EarlyInterstellar": "Early Interstellar",
"EarlyMechanical": "Early Mechanical",
"EarlyPrimitive": "Early Primitive",
"EarlySpace": "Early Space",
"LateAtomic": "Late Atomic",
"LateGalactic": "Late Galactic",
"LateInterstellar": "Late Interstellar",
"LateMechanical": "Late Mechanical",
"LatePrimitive": "Late Primitive",
"LateSpace": "Late Space",
"NoTech": "No Tech"
},
"ToggleSheet": "Toggle Sheet",
"Tooltip": {
"addTalent" : "Add Talent",
"addSkill" : "Add Skill",
"addWeapon" : "Add Weapon",
"addArmor" : "Add Armor",
"addEquipment" : "Add Equipment",
"addImplant" : "Add Implant",
"addLanguage" : "Add Language",
"addPsionic" : "Add Psionic",
"addCreatureAbility" : "Add Creature Ability",
"addCreatureTrait" : "Add Creature Trait",
"damages": "Enter current damages suffered"
},
"Vehicle": {
"FIELDS": {
"tonnage": {
"label": "Tonnage"
},
"agility": {
"label": "Agility"
},
"force": {
"label": "Force"
},
"cargo": {
"label": "Cargo"
},
"cost": {
"label": "Cost"
},
"range": {
"label": "Range"
},
"speed": {
"label": "Speed"
},
"armor": {
"label": "Armor"
},
"crew": {
"label": "Crew"
},
"description": {
"label": "Description"
},
"notes": {
"label": "Notes"
}
}
},
"Warning": {},
"Weapon": {
"FIELDS": {
"cost": {
"label": "Cost"
},
"damage": {
"label": "Damage"
},
"description": {
"label": "Description"
},
"enc": {
"label": "Enc"
},
"rangeType": {
"label": "Range"
},
"techAge": {
"label": "Tech Age"
},
"weaponType": {
"label": "Type"
}
},
"Range": {
"Handgun": "Handgun",
"Assault": "Assault",
"LongRange": "Long Range",
"Melee": "Melee",
"Rifle": "Rifle",
"HeavyWeapon": "Heavy Weapon",
"ThrownWeapon": "Thrown Weapon"
},
"Types": {
"Energy": "Energy",
"Grenade": "Grenade/Explosive",
"Heavy": "Heavy",
"Melee": "Melee",
"Projectile": "Projectile",
"Vehicle": "Vehicle"
}
}
},
"TYPES": {
"Actor": {
"character": "Character",
"creature": "Creature",
"starship": "Starship",
"vehicle": "Vehicle"
},
"Item": {
"armor": "Armor",
"creature-ability": "Creature Ability",
"creature-trait": "Creature Trait",
"equipment": "Equipment",
"implant": "Implant",
"language": "Language",
"psionic": "Psionic",
"talent": "Talent",
"weapon": "Weapon"
}
}
}

View File

@@ -0,0 +1,11 @@
export { default as HellbornWeaponSheet } from "./sheets/weapon-sheet.mjs"
export { default as HellbornVehicleSheet } from "./sheets/vehicle-sheet.mjs"
export { default as HellbornCharacterSheet } from "./sheets/character-sheet.mjs"
export { default as HellbornEquipmentSheet } from "./sheets/equipment-sheet.mjs"
export { default as HellbornCreatureSheet } from "./sheets/creature-sheet.mjs"
export { default as HellbornRitualSheet } from "./sheets/ritual-sheet.mjs"
export { default as HellbornItemSheet } from "./sheets/base-item-sheet.mjs"
export { default as HellbornCreatureSheet } from "./sheets/creature-sheet.mjs"
export { default as HellbornSpeciesTraitSheet } from "./sheets/species-trait-sheet.mjs"
export { default as HellbornPerkSheet } from "./sheets/perk-sheet.mjs"
export { default as HellbornMaleficiasSheet } from "./sheets/maleficias-sheet.mjs"

View File

@@ -0,0 +1,218 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class HellbornActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
/**
* Different sheet modes.r
* @enum {number}
*/
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#dragDrop
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-hellborn", "actor"],
position: {
width: 1400,
height: "auto",
},
form: {
submitOnChange: true,
},
window: {
resizable: true,
},
dragDrop: [{ dragSelector: '[data-drag="true"], .rollable', dropSelector: null }],
actions: {
editImage: HellbornActorSheet.#onEditImage,
toggleSheet: HellbornActorSheet.#onToggleSheet,
edit: HellbornActorSheet.#onItemEdit,
delete: HellbornActorSheet.#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(),
isEncumbered: this.document.system.isEncumbered(),
enrichedDescription: await TextEditor.enrichHTML(this.document.system.description, { async: true }),
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable,
}
return context
}
/** @override */
_onRender(context, options) {
this.#dragDrop.forEach((d) => d.bind(this.element))
// Add listeners to rollable elements
const rollables = this.element.querySelectorAll(".rollable")
rollables.forEach((d) => d.addEventListener("click", this._onRoll.bind(this)))
}
// #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 = {
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this),
}
return new DragDrop(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 true //this.isEditable && this.document.isOwner
}
/**
* Callback actions which occur when a dragged element is over a drop target.
* @param {DragEvent} event The originating DragEvent
* @protected
*/
_onDragOver(event) {}
async _onDropItem(item) {
console.log("Dropped item", item)
let 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.
*
* @this HellbornActorSheet
* @param {PointerEvent} event The originating click event
* @param {HTMLElement} target The capturing HTML element which defined a [data-action]
* @returns {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
* Start with the uuid, if it's not found, fallback to the id (as Embedded item in the actor)
* @this CthulhuEternalCharacterSheet
* @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
item = await fromUuid(uuid)
if (!item) item = this.document.items.get(id)
if (!item) return
item.sheet.render(true)
}
/**
* Delete an existing talent within the Actor
* Use the uuid to display the talent sheet
* @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
}

View File

@@ -0,0 +1,193 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class HellbornItemSheet 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: ["fvtt-hellborn", "item"],
position: {
width: 600,
height: "auto",
},
form: {
submitOnChange: true,
},
window: {
resizable: true,
},
dragDrop: [{ dragSelector: "[data-drag]", dropSelector: null }],
actions: {
toggleSheet: HellbornItemSheet.#onToggleSheet,
editImage: HellbornItemSheet.#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 = {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
item: this.document,
system: this.document.system,
source: this.document.toObject(),
enrichedDescription: await TextEditor.enrichHTML(this.document.system.description, { async: true }),
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable,
}
return context
}
/** @override */
_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 DragDrop(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) {
const el = event.currentTarget
if ("link" in event.target.dataset) return
// Extract the data you need
let dragData = null
if (!dragData) return
// Set data transfer
event.dataTransfer.setData("text/plain", JSON.stringify(dragData))
}
/**
* 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.
*
* @this CthulhuEternalCharacterSheet
* @param {PointerEvent} event The originating click event
* @param {HTMLElement} target The capturing HTML element which defined a [data-action]
* @returns {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
}

View File

@@ -0,0 +1,189 @@
import HellbornActorSheet from "./base-actor-sheet.mjs"
export default class HellbornCharacterSheet extends HellbornActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["character"],
position: {
width: 860,
height: 620,
},
window: {
contentClasses: ["character-content"],
},
actions: {
createEquipment: HellbornCharacterSheet.#onCreateEquipment,
createArmor: HellbornCharacterSheet.#onCreateArmor,
createWeapon: HellbornCharacterSheet.#onCreateWeapon,
createTalent: HellbornCharacterSheet.#onCreateTalent,
createImplant: HellbornCharacterSheet.#onCreateImplant,
createPsionic: HellbornCharacterSheet.#onCreatePsionic,
createLanguage: HellbornCharacterSheet.#onCreateLanguage
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-hellborn/templates/character-main.hbs",
},
tabs: {
template: "templates/generic/tab-navigation.hbs",
},
talents: {
template: "systems/fvtt-hellborn/templates/character-talents.hbs",
},
equipment: {
template: "systems/fvtt-hellborn/templates/character-equipment.hbs",
},
biography: {
template: "systems/fvtt-hellborn/templates/character-biography.hbs",
},
}
/** @override */
tabGroups = {
sheet: "talents",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
talents: { id: "talents", group: "sheet", icon: "fa-solid fa-compass", label: "HELLBORN.Label.talents" },
equipment: { id: "equipment", group: "sheet", icon: "fa-solid fa-shapes", label: "HELLBORN.Label.equipment" },
biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "HELLBORN.Label.biography" },
}
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()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(this.document.system.notes, { async: true })
return context
}
/** @override */
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
break
case "talents":
context.tab = context.tabs.talents
context.talents = doc.itemTypes.talent
context.talents.sort((a, b) => a.name.localeCompare(b.name))
context.implants = doc.itemTypes.implant
context.implants.sort((a, b) => a.name.localeCompare(b.name))
context.psionics = doc.itemTypes.psionic
context.psionics.sort((a, b) => a.name.localeCompare(b.name))
context.languages = doc.itemTypes.language
context.languages.sort((a, b) => a.name.localeCompare(b.name))
break
case "equipment":
context.tab = context.tabs.equipment
context.weapons = doc.itemTypes.weapon
context.weapons.sort((a, b) => a.name.localeCompare(b.name))
context.armors = doc.itemTypes.armor
context.armors.sort((a, b) => a.name.localeCompare(b.name))
context.equipments = doc.itemTypes.equipment
context.equipments.sort((a, b) => a.name.localeCompare(b.name))
break
case "biography":
context.tab = context.tabs.biography
context.enrichedDescription = await TextEditor.enrichHTML(doc.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(doc.system.notes, { async: true })
break
}
return context
}
static #onCreateEquipment(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newEquipment"), type: "equipment" }])
}
static #onCreateWeapon(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newWeapon"), type: "weapon" }])
}
static #onCreateArmor(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newArmor"), type: "armor" }])
}
static #onCreateTalent(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newTalent"), type: "talent" }])
}
static #onCreateImplant(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newImplant"), type: "implant" }])
}
static #onCreatePsionic(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newPsionic"), type: "psionic" }])
}
static #onCreateLanguage(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newLanguage"), type: "language" }])
}
/**
* Handles the roll action triggered by user interaction.
*
* @param {PointerEvent} event The event object representing the user interaction.
* @param {HTMLElement} target The target element that triggered the roll.
*
* @returns {Promise<void>} A promise that resolves when the roll action is complete.
*
* @throws {Error} Throws an error if the roll type is not recognized.
*
* @description This method checks the current mode (edit or not) and determines the type of roll
* (save, resource, or damage) based on the target element's data attributes. It retrieves the
* corresponding value from the document's system and performs the roll.
*/
async _onRoll(event, target) {
const rollType = $(event.currentTarget).data("roll-type")
let item
let li
switch (rollType) {
case "skill":
let skillId = $(event.currentTarget).data("skill-id");
item = this.actor.system.skills[skillId];
break
case "weapon":
case "damage":
li = $(event.currentTarget).parents(".item");
item = this.actor.items.get(li.data("item-id"));
break
default:
throw new Error(`Unknown roll type ${rollType}`)
}
await this.document.system.roll(rollType, item)
}
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
const data = TextEditor.getDragEventData(event)
// Handle different data types
switch (data.type) {
case "Item":
const item = await fromUuid(data.uuid)
return super._onDropItem(item)
}
}
}

View File

@@ -0,0 +1,158 @@
import HellbornActorSheet from "./base-actor-sheet.mjs"
export default class HellbornCreatureSheet extends HellbornActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["creature"],
position: {
width: 860,
height: 620,
},
window: {
contentClasses: ["creature-content"],
},
actions: {
createTrait: HellbornCreatureSheet.#onCreateTrait,
createAbility: HellbornCreatureSheet.#onCreateAbility
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-hellborn/templates/creature-main.hbs",
},
tabs: {
template: "templates/generic/tab-navigation.hbs",
},
traits: {
template: "systems/fvtt-hellborn/templates/creature-sheet-trait.hbs",
},
biography: {
template: "systems/fvtt-hellborn/templates/creature-biography.hbs",
},
}
/** @override */
tabGroups = {
sheet: "traits",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
traits: { id: "traits", group: "sheet", icon: "fa-solid fa-shapes", label: "HELLBORN.Label.traits" },
biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "HELLBORN.Label.biography" },
}
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()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(this.document.system.notes, { async: true })
return context
}
/** @override */
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
break
case "traits":
context.tab = context.tabs.traits
context.abilities = doc.itemTypes["creature-ability"]
context.abilities.sort((a, b) => a.name.localeCompare(b.name))
context.traits = doc.itemTypes["creature-trait"]
context.traits.sort((a, b) => a.name.localeCompare(b.name))
break
case "biography":
context.tab = context.tabs.biography
context.enrichedDescription = await TextEditor.enrichHTML(doc.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(doc.system.notes, { async: true })
break
}
return context
}
/**
* Creates a new attack item directly from the sheet and embeds it into the document.
* @param {Event} event The initiating click event.
* @param {HTMLElement} target The current target of the event listener.
*/
static #onCreateTrait(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newTrait"), type: "creature-trait" }])
}
static #onCreateAbility(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newAbility"), type: "creature-ability" }])
}
/**
* Handles the roll action triggered by user interaction.
*
* @param {PointerEvent} event The event object representing the user interaction.
* @param {HTMLElement} target The target element that triggered the roll.
*
* @returns {Promise<void>} A promise that resolves when the roll action is complete.
*
* @throws {Error} Throws an error if the roll type is not recognized.
*
* @description This method checks the current mode (edit or not) and determines the type of roll
* (save, resource, or damage) based on the target element's data attributes. It retrieves the
* corresponding value from the document's system and performs the roll.
*/
async _onRoll(event, target) {
const rollType = $(event.currentTarget).data("roll-type")
let item
let formula
let roll
switch (rollType) {
case "skill":
let skillId = $(event.currentTarget).data("skill-id");
item = this.actor.system.skills[skillId];
await this.document.system.roll(rollType, item)
break
case "creature-damage":
formula = this.actor.system.damage
// Rolll the damage
roll = new Roll(formula)
await roll.evaluate()
roll.toMessage( { flavor: `${this.actor.name} : Damage roll` })
break
case "creature-number":
formula = this.actor.system.numberAppearing
// Rolll the damage
roll = new Roll(formula)
await roll.evaluate()
roll.toMessage({flavor: `${this.actor.name} : Number Appearing roll`})
break
default:
throw new Error(`Unknown roll type ${rollType}`)
}
}
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
const data = TextEditor.getDragEventData(event)
// Handle different data types
switch (data.type) {
case "Item":
const item = await fromUuid(data.uuid)
return super._onDropItem(item)
}
}
}

View File

@@ -0,0 +1,28 @@
import HellbornItemSheet from "./base-item-sheet.mjs"
export default class HellbornEquipmentSheet extends HellbornItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["equipment"],
position: {
width: 600,
},
window: {
contentClasses: ["equipment-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-hellborn/templates/equipment.hbs",
},
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
return context
}
}

View File

@@ -0,0 +1,28 @@
import HellbornItemSheet from "./base-item-sheet.mjs"
export default class HellbornMaleficiasSheet extends HellbornItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["maleficias"],
position: {
width: 600,
},
window: {
contentClasses: ["maleficias-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-hellborn/templates/maleficias.hbs",
},
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
return context
}
}

View File

@@ -0,0 +1,28 @@
import HellbornItemSheet from "./base-item-sheet.mjs"
export default class HellbornPerkSheet extends HellbornItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["perk"],
position: {
width: 600,
},
window: {
contentClasses: ["perk-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-hellborn/templates/perk.hbs",
},
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
return context
}
}

View File

@@ -0,0 +1,27 @@
import HellbornItemSheet from "./base-item-sheet.mjs"
export default class HellbornRitualSheet extends HellbornItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["ritual"],
position: {
width: 600,
},
window: {
contentClasses: ["ritual-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-hellborn/templates/ritual.hbs",
},
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
return context
}
}

View File

@@ -0,0 +1,28 @@
import HellbornItemSheet from "./base-item-sheet.mjs"
export default class HellbornSpeciesTraitSheet extends HellbornItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["species-trait"],
position: {
width: 600,
},
window: {
contentClasses: ["species-trait-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-hellborn/templates/species-trait.hbs",
},
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
return context
}
}

View File

@@ -0,0 +1,134 @@
import HellbornActorSheet from "./base-actor-sheet.mjs"
export default class HellbornVehicleSheet extends HellbornActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["vehicle"],
position: {
width: 680,
height: 540,
},
window: {
contentClasses: ["vehicle-content"],
},
actions: {
createEquipment: HellbornVehicleSheet.#onCreateEquipment,
createWeapon: HellbornVehicleSheet.#onCreateWeapon,
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-hellborn/templates/vehicle-main.hbs",
},
tabs: {
template: "templates/generic/tab-navigation.hbs",
},
equipment: {
template: "systems/fvtt-hellborn/templates/vehicle-equipment.hbs",
},
description: {
template: "systems/fvtt-hellborn/templates/vehicle-description.hbs",
},
}
/** @override */
tabGroups = {
sheet: "equipment",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
equipment: { id: "equipment", group: "sheet", icon: "fa-solid fa-shapes", label: "HELLBORN.Label.equipment" },
description: { id: "description", group: "sheet", icon: "fa-solid fa-book", label: "HELLBORN.Label.description" },
}
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()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(this.document.system.notes, { async: true })
return context
}
_generateTooltip(type, target) {
}
/** @override */
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
break
case "equipment":
context.tab = context.tabs.equipment
context.weapons = doc.itemTypes.weapon
context.weapons.sort((a, b) => a.name.localeCompare(b.name))
context.equipments = doc.itemTypes.equipment
context.equipments.sort((a, b) => a.name.localeCompare(b.name))
break
case "description":
context.tab = context.tabs.description
context.enrichedDescription = await TextEditor.enrichHTML(doc.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(doc.system.notes, { async: true })
break
}
return context
}
/**
* Creates a new attack item directly from the sheet and embeds it into the document.
* @param {Event} event The initiating click event.
* @param {HTMLElement} target The current target of the event listener.
*/
static #onCreateEquipment(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newEquipment"), type: "equipment" }])
}
static #onCreateWeapon(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HELLBORN.Label.newWeapon"), type: "weapon" }])
}
async _onRoll(event, target) {
const rollType = $(event.currentTarget).data("roll-type")
let item
let li
switch (rollType) {
case "damage":
li = $(event.currentTarget).parents(".item");
item = this.actor.items.get(li.data("item-id"));
break
default:
throw new Error(`Unknown roll type ${rollType}`)
}
await this.document.system.roll(rollType, item)
}
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
const data = TextEditor.getDragEventData(event)
// Handle different data types
switch (data.type) {
case "Item":
const item = await fromUuid(data.uuid)
return super._onDropItem(item)
}
}
}

View File

@@ -0,0 +1,21 @@
import HellbornItemSheet from "./base-item-sheet.mjs"
export default class HellbornWeaponSheet extends HellbornItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["weapon"],
position: {
width: 620,
},
window: {
contentClasses: ["weapon-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-hellborn/templates/weapon.hbs",
},
}
}

154
module/config/system.mjs Normal file
View File

@@ -0,0 +1,154 @@
export const SYSTEM_ID = "fvtt-hellborn"
export const ASCII = `
░▒▓████████▓▒░▒▓████████▓▒░▒▓█▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░░▒▓██████████████▓▒░ ░▒▓██████▓▒░░▒▓███████▓▒░
░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓██▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓██▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
░▒▓██████▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░
░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓██▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓██▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓████████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓██████▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░
`
export const SKILLS = {
"combat": { id: "combat", label: "HELLBORN.Skill.Combat" },
"knowledge": { id: "knowledge", label: "HELLBORN.Skill.Knowledge" },
"social": { id: "social", label: "HELLBORN.Skill.Social" },
"physical": { id: "physical", label: "HELLBORN.Skill.Physical" },
"stealth": { id: "stealth", label: "HELLBORN.Skill.Stealth" },
"vehicles": { id: "vehicles", label: "HELLBORN.Skill.Vehicles" },
"technology": { id: "technology", label: "HELLBORN.Skill.Technology" }
}
export const TECH_AGES = {
"notech": { id: "notech", level: 0, label: "HELLBORN.TechAge.NoTech" },
"earlyprimitive": { id: "earlyprimitive", label: "HELLBORN.TechAge.EarlyPrimitive", level: 1 },
"lateprimitive": { id: "lateprimitive", label: "HELLBORN.TechAge.LatePrimitive", level: 2 },
"earlymechanical": { id: "earlymechanical", label: "HELLBORN.TechAge.EarlyMechanical", level: 3 },
"latemechanical": { id: "latemechanical", label: "HELLBORN.TechAge.LateMechanical", level: 4 },
"earlyatomic": { id: "earlyatomic", label: "HELLBORN.TechAge.EarlyAtomic", level: 5 },
"lateatomic": { id: "lateatomic", label: "HELLBORN.TechAge.LateAtomic", level: 6 },
"earlyspace": { id: "earlyspace", label: "HELLBORN.TechAge.EarlySpace", level: 7 },
"latespace": { id: "latespace", label: "HELLBORN.TechAge.LateSpace", level: 8 },
"earlyinterstellar": { id: "earlyinterstellar", label: "HELLBORN.TechAge.EarlyInterstellar", level: 9 },
"lateinterstellar": { id: "lateinterstellar", label: "HELLBORN.TechAge.LateInterstellar", level: 10 },
"earlygalactic": { id: "earlygalactic", label: "HELLBORN.TechAge.EarlyGalactic", level: 11 },
"lategalactic": { id: "lategalactic", label: "HELLBORN.TechAge.LateGalactic", level: 12 },
"cosmic": { id: "cosmic", label: "HELLBORN.TechAge.Cosmic", level: 13 }
}
export const WEAPON_TYPES = {
"melee": { id: "melee", label: "HELLBORN.Weapon.Types.Melee" },
"projectile": { id: "projectile", label: "HELLBORN.Weapon.Types.Projectile" },
"energy": { id: "energy", label: "HELLBORN.Weapon.Types.Energy" },
"heavy": { id: "heavy", label: "HELLBORN.Weapon.Types.Heavy" },
"grenade": { id: "grenade", label: "HELLBORN.Weapon.Types.Grenade" },
"vehicle": { id: "vehicle", label: "HELLBORN.Weapon.Types.Vehicle" }
}
export const WEAPON_RANGE = {
"handgun": { id: "handgun", label: "HELLBORN.Weapon.Range.Handgun", range: {close: 0, near:0, far:-2} },
"assault": { id: "assault", label: "HELLBORN.Weapon.Range.Assault", range: {close: -2, near:0, far:-1, distant: -2} },
"rifle": { id: "rifle", label: "HELLBORN.Weapon.Range.Rifle", range: {close: -3, near:0, far:0, distant: -1} },
"melee": { id: "melee", label: "HELLBORN.Weapon.Range.Melee", range: {close: 0} },
"heavyweapon": { id: "heavyweapon", label: "HELLBORN.Weapon.Range.HeavyWeapon", range: {near:-1, far:0, distant: 0} },
"thrownweapon": { id: "thrownweapon", label: "HELLBORN.Weapon.Range.ThrownWeapon", range: {close: 0, near:-1} }
}
export const ATTACK_MODIFIERS = {
"two-attacks": -1,
"aiming": 1,
"dim": -1,
"darkness": -2,
"prone": -1,
"cover": -2,
"recoil-first": -1,
"recoil-third": -2,
"aware": -1
}
export const TRIAGE_RESULTS = {
"none": { id: "none", dice:0, label: "HELLBORN.TriageResults.None" },
"death": { id: "death", dice:3, label: "HELLBORN.TriageResults.Death" },
"critical": { id: "critical", dice:4, label: "HELLBORN.TriageResults.Critical" },
"severe": { id: "severe", dice:7, label: "HELLBORN.TriageResults.Severe" },
"moderate": { id: "moderate", dice:10, label: "HELLBORN.TriageResults.Moderate" },
"fleshwound": { id: "fleshwound", dice:12, label: "HELLBORN.TriageResults.FleshWound" }
}
export const CREATURE_TERRAIN_TYPES = {
"cave": { id: "cave", label: "HELLBORN.Creature.Terrain.Cave", niche:0, size: 0 },
"coast": { id: "coast", label: "HELLBORN.Creature.Terrain.Coast", niche:1, size: 0 },
"desert": { id: "desert", label: "HELLBORN.Creature.Terrain.Desert", niche:-1, size: -1 },
"forest": { id: "forest", label: "HELLBORN.Creature.Terrain.Forest", niche:1, size: 1 },
"jungle": { id: "jungle", label: "HELLBORN.Creature.Terrain.Jungle", niche:1, size: 1 },
"mixed": { id: "mixed", label: "HELLBORN.Creature.Terrain.Mixed", niche:0, size: 0 },
"mountain": { id: "mountain", label: "HELLBORN.Creature.Terrain.Mountain", niche:-1, size: -1 },
"ocean": { id: "ocean", label: "HELLBORN.Creature.Terrain.Ocean", niche:-1, size: 1 },
"river": { id: "river", label: "HELLBORN.Creature.Terrain.River", niche:1, size: 0 },
"ruins": { id: "ruins", label: "HELLBORN.Creature.Terrain.Ruins", niche:0, size: 1 },
"savannah": { id: "savannah", label: "HELLBORN.Creature.Terrain.Savannah", niche:0, size: 1 },
"shallows": { id: "shallows", label: "HELLBORN.Creature.Terrain.Shallows", niche:1, size: 0 },
"swamp": { id: "swamp", label: "HELLBORN.Creature.Terrain.Swamp", niche:1, size: 1 }
}
export const CREATURE_NICHES = {
"prey": { id: "prey", label: "HELLBORN.Creature.Niche.Prey" },
"opportunist": { id: "opportunist", label: "HELLBORN.Creature.Niche.Opportunist" },
"herbivore": { id: "herbivore", label: "HELLBORN.Creature.Niche.Herbivore" },
"predator": { id: "predator", label: "HELLBORN.Creature.Niche.Predator" }
}
export const CREATURE_SIZES = {
"tiny": { id: "tiny", label: "HELLBORN.Creature.Size.Tiny" },
"small": { id: "small", label: "HELLBORN.Creature.Size.Small" },
"medium": { id: "medium", label: "HELLBORN.Creature.Size.Medium" },
"large": { id: "large", label: "HELLBORN.Creature.Size.Large" },
"giant": { id: "giant", label: "HELLBORN.Creature.Size.Giant" },
"titanic": { id: "titanic", label: "HELLBORN.Creature.Size.Titanic" }
}
export const MODIFIER_CHOICES = {
"easy": { id: "easy", label: "HELLBORN.Label.Easy", value :"1" },
"moderate": { id: "moderate", label: "HELLBORN.Label.Moderate", value: "0" },
"difficult": { id: "difficult", label: "HELLBORN.Label.Difficult", value: "-1" },
"formidable": { id: "formidable", label: "HELLBORN.Label.Formidable", value: "-2" },
"impossible": { id: "impossible", label: "HELLBORN.Label.Impossible", value: "-4" }
}
export const STARSHIP_HULL = {
"pod": { id: "pod", label: "HELLBORN.Starship.Hull.Pod" },
"micro": { id: "micro", label: "HELLBORN.Starship.Hull.Micro" },
"small": { id: "small", label: "HELLBORN.Starship.Hull.Small" },
"scout": { id: "scout", label: "HELLBORN.Starship.Hull.Scout" },
"picket": { id: "picket", label: "HELLBORN.Starship.Hull.Picket" },
"destroyer": { id: "destroyer", label: "HELLBORN.Starship.Hull.Destroyer" },
"cruiser": { id: "cruiser", label: "HELLBORN.Starship.Hull.Cruiser" },
"battleship": { id: "battleship", label: "HELLBORN.Starship.Hull.Battleship" },
"carrier": { id: "carrier", label: "HELLBORN.Starship.Hull.Carrier" }
}
/**
* Include all constant definitions within the SYSTEM global export
* @type {Object}
*/
export const SYSTEM = {
id: SYSTEM_ID,
MODIFIER_CHOICES,
ATTACK_MODIFIERS,
TECH_AGES,
WEAPON_TYPES,
WEAPON_RANGE,
TRIAGE_RESULTS,
CREATURE_TERRAIN_TYPES,
CREATURE_SIZES,
CREATURE_NICHES,
STARSHIP_HULL,
SKILLS,
ASCII
}

View File

@@ -0,0 +1,4 @@
export { default as HellbornActor } from "./actor.mjs"
export { default as HellbornItem } from "./item.mjs"
export { default as HellbornRoll } from "./roll.mjs"
export { default as HellbornChatMessage } from "./chat-message.mjs"

View File

@@ -0,0 +1,53 @@
import HellbornUtils from "../utils.mjs"
export default class HellbornActor extends Actor {
static async create(data, options) {
// Case of compendium global import
if (data instanceof Array) {
return super.create(data, options);
}
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
if (data.items) {
let actor = super.create(data, options);
return actor;
}
if (data.type === 'character') {
}
return super.create(data, options);
}
_onUpdate(changed, options, userId) {
// DEBUG : console.log("CthulhuEternalActor.update", changed, options, userId)
if (changed?.system?.wp?.exhausted) {
ChatMessage.create({
user: userId,
speaker: { alias: this.name },
rollMode: "selfroll",
content: game.i18n.localize("HELLBORN.ChatMessage.exhausted"),
type: CONST.CHAT_MESSAGE_STYLES.OTHER
})
}
return super._onUpdate(changed, options, userId)
}
async _preCreate(data, options, user) {
await super._preCreate(data, options, user)
// Configure prototype token settings
const prototypeToken = {}
if (this.type === "character") {
Object.assign(prototypeToken, {
sight: { enabled: true },
actorLink: true,
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY,
})
this.updateSource({ prototypeToken })
}
}
}

View File

@@ -0,0 +1,21 @@
import HellbornRoll from "./roll.mjs"
export default class HellbornChatMessage extends ChatMessage {
async _renderRollContent(messageData) {
const data = messageData.message
if (this.rolls[0] instanceof HellbornRoll) {
const isPrivate = !this.isContentVisible
// _renderRollHTML va appeler render sur tous les rolls
const rollHTML = await this._renderRollHTML(isPrivate)
if (isPrivate) {
data.flavor = game.i18n.format("CHAT.PrivateRollContent", { user: this.user.name })
messageData.isWhisper = false
messageData.alias = this.user.name
}
data.content = `<section class="dice-rolls">${rollHTML}</section>`
return
}
return super._renderRollContent(messageData)
}
}

17
module/documents/item.mjs Normal file
View File

@@ -0,0 +1,17 @@
export const defaultItemImg = {
weapon: "systems/fvtt-hellborn/assets/icons/icon_weapon.svg",
equipment: "systems/fvtt-hellborn/assets/icons/icon_equipment.svg",
ritual: "systems/fvtt-hellborn/assets/icons/icon_psionic.svg",
maleficias: "systems/fvtt-hellborn/assets/icons/icon_talent.svg",
perk: "systems/fvtt-hellborn/assets/icons/icon_language.svg",
"species-trait": "systems/fvtt-hellborn/assets/icons/icon_creature_trait.svg"
}
export default class HellbornItem extends Item {
constructor(data, context) {
if (!data.img) {
data.img = defaultItemImg[data.type];
}
super(data, context);
}
}

350
module/documents/roll.mjs Normal file
View File

@@ -0,0 +1,350 @@
import { SYSTEM } from "../config/system.mjs"
export default class HellbornRoll extends Roll {
/**
* The HTML template path used to render dice checks of this type
* @type {string}
*/
static CHAT_TEMPLATE = "systems/fvtt-hellborn/templates/chat-message.hbs"
get type() {
return this.options.type
}
get isDamage() {
return this.type === ROLL_TYPE.DAMAGE
}
get target() {
return this.options.target
}
get value() {
return this.options.value
}
get actorId() {
return this.options.actorId
}
get actorName() {
return this.options.actorName
}
get actorImage() {
return this.options.actorImage
}
get help() {
return this.options.help
}
get resultType() {
return this.options.resultType
}
get isFailure() {
return this.resultType === "failure"
}
get hasTarget() {
return this.options.hasTarget
}
get realDamage() {
return this.options.realDamage
}
get weapon() {
return this.options.weapon
}
static updateFullFormula(options) {
let fullFormula
if ( options.numericModifier >= 0) {
fullFormula = `${options.formula} + ${options.rollItem.value} + ${options.numericModifier}D`
} else {
fullFormula = `${options.formula} + ${options.rollItem.value} - ${Math.abs(options.numericModifier)}D`
}
$('#roll-dialog-full-formula').text(fullFormula)
options.fullFormula = fullFormula
}
/**
* Prompt the user with a dialog to configure and execute a roll.
*
* @param {Object} options Configuration options for the roll.
* @param {string} options.rollType The type of roll being performed.
* @param {string} options.rollTarget The target of the roll.
* @param {string} options.actorId The ID of the actor performing the roll.
* @param {string} options.actorName The name of the actor performing the roll.
* @param {string} options.actorImage The image of the actor performing the roll.
* @param {boolean} options.hasTarget Whether the roll has a target.
* @param {Object} options.data Additional data for the roll.
*
* @returns {Promise<Object|null>} The roll result or null if the dialog was cancelled.
*/
static async prompt(options = {}) {
let formula = "2d6"
switch (options.rollType) {
case "skill":
break
case "damage":
let formula = options.rollItem.system.damage
let damageRoll = new Roll(formula)
await damageRoll.evaluate()
await damageRoll.toMessage({
flavor: `${options.rollItem.name} - Damage Roll`
});
return
case "weapon":
let actor = game.actors.get(options.actorId)
options.weapon = foundry.utils.duplicate(options.rollItem)
options.rollItem = actor.system.skills.combat
break
default:
break
}
const rollModes = Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)]))
const fieldRollMode = new foundry.data.fields.StringField({
choices: rollModes,
blank: false,
default: "public",
})
const choiceModifier = SYSTEM.MODIFIER_CHOICES
let choiceRangeModifier = {}
let rangeModifier = 0
if ( options.weapon) {
// Build the range modifiers
let range = SYSTEM.WEAPON_RANGE[options.weapon.system.rangeType]
for (let [key, value] of Object.entries(range.range)) {
choiceRangeModifier[key] = { label: `${key} (${value}D)`, value: value }
if (!rangeModifier && value) {
rangeModifier = value
}
}
}
let modifier = "0"
options.numericModifier = rangeModifier
let fullFormula = `${formula} + ${options.rollItem.value}`
if (options.isEncumbered) {
options.numericModifier += -1
fullFormula += ` - ${options.numericModifier}D`
} else {
options.numericModifier += 0
fullFormula += ` + ${options.numericModifier}D`
}
options.fullFormula = fullFormula
options.formula = formula
let dialogContext = {
actorId: options.actorId,
actorName: options.actorName,
rollType: options.rollType,
rollItem: foundry.utils.duplicate(options.rollItem), // Object only, no class
fullFormula,
weapon: options?.weapon,
isEncumbered: options.isEncumbered,
talents: options.talents,
rollModes,
fieldRollMode,
choiceModifier,
choiceRangeModifier,
rangeModifier,
formula,
hasTarget: options.hasTarget,
modifier,
}
const content = await renderTemplate("systems/fvtt-hellborn/templates/roll-dialog.hbs", dialogContext)
const title = HellbornRoll.createTitle(options.rollType, options.rollTarget)
const label = game.i18n.localize("HELLBORN.Roll.roll")
const rollContext = await foundry.applications.api.DialogV2.wait({
window: { title: title },
classes: ["fvtt-hellborn"],
content,
buttons: [
{
label: label,
callback: (event, button, dialog) => {
const output = Array.from(button.form.elements).reduce((obj, input) => {
if (input.name) obj[input.name] = input.value
return obj
}, {})
return output
},
},
],
actions: {
},
rejectClose: false, // Click on Close button will not launch an error
render: (event, dialog) => {
$(".roll-skill-modifier").change(event => {
options.numericModifier += Number(event.target.value)
HellbornRoll.updateFullFormula(options)
})
$(".roll-skill-range-modifier").change(event => {
options.numericModifier += Number(event.target.value)
HellbornRoll.updateFullFormula(options)
})
$(".select-combat-option").change(event => {
console.log(event)
let field = $(event.target).data("field")
let modifier = SYSTEM.ATTACK_MODIFIERS[field]
if ( event.target.checked) {
options.numericModifier += modifier
} else {
options.numericModifier -= modifier
}
HellbornRoll.updateFullFormula(options)
})
}
})
// If the user cancels the dialog, exit
if (rollContext === null) return
let rollData = foundry.utils.mergeObject(foundry.utils.duplicate(options), rollContext)
rollData.rollMode = rollContext.visibility
// Update target score
rollData.targetScore = 8
if (Hooks.call("fvtt-hellborn.preRoll", options, rollData) === false) return
let diceFormula = `${2+Math.abs(options.numericModifier)}D6`
if ( options.numericModifier > 0 ) {
diceFormula += `kh2 + ${options.rollItem.value}`
} else {
diceFormula += `kl2 + ${options.rollItem.value}`
}
const roll = new this(diceFormula, options.data, rollData)
await roll.evaluate()
roll.displayRollResult(roll, options, rollData)
if (Hooks.call("fvtt-hellborn.Roll", options, rollData, roll) === false) return
return roll
}
displayRollResult(formula, options, rollData) {
// Compute the result quality
let resultType = "failure"
if (this.total >= 8) {
resultType = "success"
// Detect if decimal == unit in the dire total result
}
this.options.resultType = resultType
this.options.isSuccess = resultType === "success"
this.options.isFailure = resultType === "failure"
this.options.isEncumbered = rollData.isEncumbered
this.options.rollData = foundry.utils.duplicate(rollData)
}
/**
* Creates a title based on the given type.
*
* @param {string} type The type of the roll.
* @param {string} target The target of the roll.
* @returns {string} The generated title.
*/
static createTitle(type, target) {
switch (type) {
case "skill":
return `${game.i18n.localize("HELLBORN.Label.titleSkill")}`
case "weapon":
return `${game.i18n.localize("HELLBORN.Label.titleWeapon")}`
default:
return game.i18n.localize("HELLBORN.Label.titleStandard")
}
}
/** @override */
async render(chatOptions = {}) {
let chatData = await this._getChatCardData(chatOptions.isPrivate)
return await renderTemplate(this.constructor.CHAT_TEMPLATE, chatData)
}
/**
* Generates the data required for rendering a roll chat card.
*
* @param {boolean} isPrivate Indicates if the chat card is private.
* @returns {Promise<Object>} A promise that resolves to an object containing the chat card data.
* @property {Array<string>} css - CSS classes for the chat card.
* @property {Object} data - The data associated with the roll.
* @property {number} diceTotal - The total value of the dice rolled.
* @property {boolean} isGM - Indicates if the user is a Game Master.
* @property {string} formula - The formula used for the roll.
* @property {number} total - The total result of the roll.
* @property {boolean} isFailure - Indicates if the roll is a failure.
* @property {string} actorId - The ID of the actor performing the roll.
* @property {string} actingCharName - The name of the character performing the roll.
* @property {string} actingCharImg - The image of the character performing the roll.
* @property {string} resultType - The type of result (e.g., success, failure).
* @property {boolean} hasTarget - Indicates if the roll has a target.
* @property {string} targetName - The name of the target.
* @property {number} targetArmor - The armor value of the target.
* @property {number} realDamage - The real damage dealt.
* @property {boolean} isPrivate - Indicates if the chat card is private.
* @property {string} cssClass - The combined CSS classes as a single string.
* @property {string} tooltip - The tooltip text for the chat card.
*/
async _getChatCardData(isPrivate) {
let cardData = foundry.utils.duplicate(this.options)
cardData.css = [SYSTEM.id, "dice-roll"]
cardData.data = this.data
cardData.diceTotal = this.dice.reduce((t, d) => t + d.total, 0)
cardData.isGM = game.user.isGM
cardData.formula = this.formula
cardData.fullFormula = this.options.fullFormula
cardData.numericModifier = this.options.numericModifier
cardData.total = this.total
cardData.actorId = this.actorId
cardData.actingCharName = this.actorName
cardData.actingCharImg = this.actorImage
cardData.resultType = this.resultType
cardData.hasTarget = this.hasTarget
cardData.targetName = this.targetName
cardData.targetArmor = this.targetArmor
cardData.realDamage = this.realDamage
cardData.isPrivate = isPrivate
cardData.weapon = this.weapon
cardData.isEncumbered = this.isEncumbered
cardData.cssClass = cardData.css.join(" ")
cardData.tooltip = isPrivate ? "" : await this.getTooltip()
return cardData
}
/**
* Converts the roll result to a chat message.
*
* @param {Object} [messageData={}] Additional data to include in the message.
* @param {Object} options Options for message creation.
* @param {string} options.rollMode The mode of the roll (e.g., public, private).
* @param {boolean} [options.create=true] Whether to create the message.
* @returns {Promise} - A promise that resolves when the message is created.
*/
async toMessage(messageData = {}, { rollMode, create = true } = {}) {
super.toMessage(
{
isFailure: this.resultType === "failure",
actingCharName: this.actorName,
actingCharImg: this.actorImage,
hasTarget: this.hasTarget,
realDamage: this.realDamage,
...messageData,
},
{ rollMode: rollMode },
)
}
}

View File

@@ -0,0 +1,9 @@
export { default as HellbornCreature } from "./creature.mjs"
export { default as HellbornVehicle } from "./vehicle.mjs"
export { default as HellbornCharacter } from "./character.mjs"
export { default as HellbornEquipment } from "./equipment.mjs"
export { default as HellbornRitual } from "./ritual.mjs"
export { default as HellbornPerk } from "./perk.mjs"
export { default as HellbornMaleficias } from "./maleficias.mjs"
export { default as HellbornSpeciesTrait } from "./species-trait.mjs"
export { default as HellbornWeapon } from "./weapon.mjs"

136
module/models/character.mjs Normal file
View File

@@ -0,0 +1,136 @@
import { SYSTEM } from "../config/system.mjs"
import HellbornRoll from "../documents/roll.mjs"
export default class HellbornActor 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 })
schema.name = new fields.StringField({ required: true, nullable: false, initial: "" })
schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" })
schema.species = new fields.StringField({ required: true, nullable: false, initial: "" })
schema.archetype = new fields.StringField({ required: true, nullable: false, initial: "" })
// Carac
const skillField = (label) => {
const schema = {
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 5 }),
label: new fields.StringField({ required: true, nullable: false, initial: label })
}
return new fields.SchemaField(schema, { label })
}
schema.skills = new fields.SchemaField(
Object.values(SYSTEM.SKILLS).reduce((obj, characteristic) => {
obj[characteristic.id] = skillField(characteristic.label)
return obj
}, {}),
)
schema.heroPoints = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.health = new fields.SchemaField({
staminaValue: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
staminaMax: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
wounds: new fields.NumberField({ ...requiredInteger, initial:0, min: 0 }),
triageResults: new fields.StringField({ required: true, nullable: false, initial: "none", choices: SYSTEM.TRIAGE_RESULTS })
})
schema.enc = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
})
schema.armor = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
})
schema.credits = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.rank = new fields.SchemaField({
experienced: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 5 }),
expert: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 5 }),
veteran: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 5 }),
elite: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 5 }),
legend: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 5 })
})
schema.biodata = new fields.SchemaField({
age: new fields.NumberField({ ...requiredInteger, initial: 15, min: 6 }),
height: new fields.NumberField({ ...requiredInteger, initial: 170, min: 50 }),
weight: new fields.NumberField({ ...requiredInteger, initial: 70, min: 1 }),
gender: new fields.StringField({ required: true, nullable: false, initial: "" }),
home: new fields.StringField({ required: true, nullable: false, initial: "" }),
birthplace: new fields.StringField({ required: true, nullable: false, initial: "" }),
eyes: new fields.StringField({ required: true, nullable: false, initial: "" }),
hair: new fields.StringField({ required: true, nullable: false, initial: "" })
})
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["HELLBORN.Character"]
prepareDerivedData() {
super.prepareDerivedData();
let encMax = 10 + (2*this.skills.physical.value)
if (encMax !== this.enc.max) {
this.enc.max = encMax
}
let enc = 0
let armor = 0
for (let i of this.parent.items) {
if (i.system?.enc) {
enc += i.system.enc
}
if ( i.system?.protection) {
armor += i.system.protection
}
}
if (enc !== this.enc.value) {
this.enc.value = enc
}
if (armor !== this.armor.value) {
this.armor.value = armor
}
let staminaMax = 14 + (3*this.skills.physical.value)
if (staminaMax !== this.health.staminaMax) {
this.health.staminaMax = staminaMax
}
}
isEncumbered() {
return this.enc.value > this.enc.max
}
/** */
/**
* Rolls a dice for a character.
* @param {("save"|"resource|damage")} rollType The type of the roll.
* @param {number} rollItem The target value for the roll. Which caracteristic or resource. If the roll is a damage roll, this is the id of the item.
* @returns {Promise<null>} - A promise that resolves to null if the roll is cancelled.
*/
async roll(rollType, rollItem) {
let opponentTarget
const hasTarget = opponentTarget !== undefined
let roll = await HellbornRoll.prompt({
rollType,
rollItem,
actorId: this.parent.id,
actorName: this.parent.name,
actorImage: this.parent.img,
talents: this.parent.items.filter(i => i.type === "talent" && i.system.isAdvantage),
isEncumbered: this.isEncumbered(),
hasTarget,
target: opponentTarget
})
if (!roll) return null
await roll.toMessage({}, { rollMode: roll.options.rollMode })
}
}

View File

@@ -0,0 +1,72 @@
import { SYSTEM } from "../config/system.mjs"
import HellbornRoll from "../documents/roll.mjs"
export default class HellbornCreature extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
// Carac
const skillField = (label) => {
const schema = {
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 5 }),
label: new fields.StringField({ required: true, nullable: false, initial: label }),
enabled: new fields.BooleanField({ required: true, initial: true }),
}
return new fields.SchemaField(schema, { label })
}
schema.skills = new fields.SchemaField(
Object.values(SYSTEM.SKILLS).reduce((obj, characteristic) => {
obj[characteristic.id] = skillField(characteristic.label)
return obj
}, {}),
)
schema.terrain = new fields.StringField({ required: true, nullable: false, initial: "cave", choices: SYSTEM.CREATURE_TERRAIN_TYPES })
schema.niche = new fields.StringField({ required: true, nullable: false, initial: "prey", choices: SYSTEM.CREATURE_NICHES })
schema.size = new fields.StringField({ required: true, nullable: false, initial: "small", choices: SYSTEM.CREATURE_SIZES })
schema.numberAppearing = new fields.StringField({ required: true, initial: "1d6" })
schema.health = new fields.SchemaField({
staminaValue: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
staminaMax: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 }),
})
schema.damage = new fields.StringField({ required: true, initial: "1d6" })
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["HELLBORN.Creature"]
isEncumbered() {
return false
}
async roll(rollType, rollItem) {
let opponentTarget
const hasTarget = opponentTarget !== undefined
let roll = await HellbornRoll.prompt({
rollType,
rollItem,
actorId: this.parent.id,
actorName: this.parent.name,
actorImage: this.parent.img,
traits: this.parent.items.filter(i => i.type === "creature-trait" && i.system.isAdvantage),
abilities: this.parent.items.filter(i => i.type === "creature-ability" && i.system.isAdvantage),
isEncumbered: this.isEncumbered(),
hasTarget,
target: opponentTarget
})
if (!roll) return null
await roll.toMessage({}, { rollMode: roll.options.rollMode })
}
}

View File

@@ -0,0 +1,22 @@
import { SYSTEM } from "../config/system.mjs"
export default class HellbornEquipment extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
const requiredInteger = { required: true, nullable: false, integer: true }
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.techAge = new fields.StringField({ required: true, choices: SYSTEM.TECH_AGES, initial : "lateatomic" })
schema.enc = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
schema.cost = new fields.NumberField({ required: true, initial: 0, min: 0 })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["HELLBORN.Equipment"]
}

View File

@@ -0,0 +1,22 @@
import { SYSTEM } from "../config/system.mjs"
export default class HellbornMaleficias extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
const requiredInteger = { required: true, nullable: false, integer: true }
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.techAge = new fields.StringField({ required: true, choices: SYSTEM.TECH_AGES, initial : "lateatomic" })
schema.enc = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
schema.cost = new fields.NumberField({ required: true, initial: 0, min: 0 })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["HELLBORN.Implant"]
}

17
module/models/perk.mjs Normal file
View File

@@ -0,0 +1,17 @@
import { SYSTEM } from "../config/system.mjs"
export default class HellbornPerk extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
const requiredInteger = { required: true, nullable: false, integer: true }
schema.description = new fields.HTMLField({ required: true, textSearch: true })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["HELLBORN.Psionic"]
}

20
module/models/ritual.mjs Normal file
View File

@@ -0,0 +1,20 @@
import { SYSTEM } from "../config/system.mjs"
export default class HellbornRitual 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: false,
blank: true,
initial: "",
textSearch: true,
})
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["HELLBORN.Language"]
}

View File

@@ -0,0 +1,16 @@
import { SYSTEM } from "../config/system.mjs";
export default class HellbornSpeciesTrait extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
const schema = {};
schema.isAdvantage = new fields.BooleanField({ required: true, initial: false });
schema.description = new fields.HTMLField({ required: true, textSearch: true })
return schema;
}
/** @override */
static LOCALIZATION_PREFIXES = ["HELLBORN.CreatureTrait"];
}

55
module/models/vehicle.mjs Normal file
View File

@@ -0,0 +1,55 @@
import { SYSTEM } from "../config/system.mjs"
import HellbornRoll from "../documents/roll.mjs"
export default class HellbornVehicle extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
schema.agility = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.armor = new fields.StringField({ required: true, initial: "" })
schema.cargo = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.crew = new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 })
schema.force = new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 })
schema.range = new fields.StringField({ required: true, initial: "1d6" })
schema.speed = new fields.StringField({ required: true, initial: "1d6" })
schema.techAge = new fields.StringField({ required: true, initial: "1d6" })
schema.tonnage = new fields.NumberField({ required: true, initial: 1, min: 0 })
schema.damages = new fields.StringField({ required: true, initial: "" })
schema.cost = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["HELLBORN.Vehicle"]
isEncumbered() {
return false
}
async roll(rollType, rollItem) {
let opponentTarget
const hasTarget = opponentTarget !== undefined
let roll = await HellbornRoll.prompt({
rollType,
rollItem,
actorId: this.parent.id,
actorName: this.parent.name,
actorImage: this.parent.img,
isEncumbered: this.isEncumbered(),
hasTarget,
target: opponentTarget
})
if (!roll) return null
await roll.toMessage({}, { rollMode: roll.options.rollMode })
}
}

37
module/models/weapon.mjs Normal file
View File

@@ -0,0 +1,37 @@
import { SYSTEM } from "../config/system.mjs"
export default class HellbornWeapon extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
const requiredInteger = { required: true, nullable: false, integer: true }
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.techAge = new fields.StringField({ required: true, choices: SYSTEM.TECH_AGES, initial : "lateatomic" })
schema.weaponType = new fields.StringField({ required: true, initial: "melee", choices: SYSTEM.WEAPON_TYPES })
schema.rangeType = new fields.StringField({ required: true, initial: "melee", choices: SYSTEM.WEAPON_RANGE })
schema.damage = new fields.StringField({required: true, initial: "1d6"})
schema.magazine = new fields.NumberField({ required: true, initial: 1, min: 0 })
schema.range = new fields.SchemaField({
close: new fields.NumberField({ ...requiredInteger, initial: 0 }),
near: new fields.NumberField({ ...requiredInteger, initial: 0 }),
far: new fields.NumberField({ ...requiredInteger, initial: 0 }),
dist: new fields.NumberField({ ...requiredInteger, initial: 0 }),
})
schema.enc = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.aspect = new fields.StringField({ required: true, initial: ""})
schema.cost = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.ammoCost = new fields.NumberField({ required: true, initial: 0, min: 0 })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["FTLNOMAD.Weapon"]
}

13
module/socket.mjs Normal file
View File

@@ -0,0 +1,13 @@
/**
* Handles socket events based on the provided action.
*
* @param {Object} [params={}] The parameters for the socket event.
* @param {string|null} [params.action=null] The action to be performed.
* @param {Object} [params.data={}] The data associated with the action.
* @returns {*} The result of the action handler, if applicable.
*/
export function handleSocketEvent({ action = null, data = {} } = {}) {
console.debug("handleSocketEvent", action, data)
}

198
module/utils.mjs Normal file
View File

@@ -0,0 +1,198 @@
import { SYSTEM } from "./config/system.mjs"
export default class HellbornUtils {
static registerSettings() {
game.settings.register("fvtt-hellborn", "settings-era", {
name: game.i18n.localize("FTLNOMAD.Settings.era"),
hint: game.i18n.localize("HELLBORN.Settings.eraHint"),
default: "jazz",
scope: "world",
type: String,
choices: SYSTEM.AVAILABLE_SETTINGS,
config: true,
onChange: _ => window.location.reload()
});
}
static async loadCompendiumData(compendium) {
const pack = game.packs.get(compendium)
return await pack?.getDocuments() ?? []
}
static async loadCompendium(compendium, filter = item => true) {
let compendiumData = await HellbornUtils.loadCompendiumData(compendium)
return compendiumData.filter(filter)
}
static registerHandlebarsHelpers() {
Handlebars.registerHelper('isNull', function (val) {
return val == null;
});
Handlebars.registerHelper('exists', function (val) {
return val != null && val !== undefined;
});
Handlebars.registerHelper('isEmpty', function (list) {
if (list) return list.length === 0;
else return false;
});
Handlebars.registerHelper('notEmpty', function (list) {
return list.length > 0;
});
Handlebars.registerHelper('isNegativeOrNull', function (val) {
return val <= 0;
});
Handlebars.registerHelper('isNegative', function (val) {
return val < 0;
});
Handlebars.registerHelper('isPositive', function (val) {
return val > 0;
});
Handlebars.registerHelper('equals', function (val1, val2) {
return val1 === val2;
});
Handlebars.registerHelper('neq', function (val1, val2) {
return val1 !== val2;
});
Handlebars.registerHelper('gt', function (val1, val2) {
return val1 > val2;
})
Handlebars.registerHelper('lt', function (val1, val2) {
return val1 < val2;
})
Handlebars.registerHelper('gte', function (val1, val2) {
return val1 >= val2;
})
Handlebars.registerHelper('lte', function (val1, val2) {
return val1 <= val2;
})
Handlebars.registerHelper('and', function (val1, val2) {
return val1 && val2;
})
Handlebars.registerHelper('or', function (val1, val2) {
return val1 || val2;
})
Handlebars.registerHelper('or3', function (val1, val2, val3) {
return val1 || val2 || val3;
})
Handlebars.registerHelper('for', function (from, to, incr, block) {
let accum = '';
for (let i = from; i < to; i += incr)
accum += block.fn(i);
return accum;
})
Handlebars.registerHelper('not', function (cond) {
return !cond;
})
Handlebars.registerHelper('count', function (list) {
return list.length;
})
Handlebars.registerHelper('countKeys', function (obj) {
return Object.keys(obj).length;
})
Handlebars.registerHelper('isEnabled', function (configKey) {
return game.settings.get("bol", configKey);
})
Handlebars.registerHelper('split', function (str, separator, keep) {
return str.split(separator)[keep];
})
// If you need to add Handlebars helpers, here are a few useful examples:
Handlebars.registerHelper('concat', function () {
let outStr = '';
for (let arg in arguments) {
if (typeof arguments[arg] != 'object') {
outStr += arguments[arg];
}
}
return outStr;
})
Handlebars.registerHelper('add', function (a, b) {
return parseInt(a) + parseInt(b);
});
Handlebars.registerHelper('mul', function (a, b) {
return parseInt(a) * parseInt(b);
})
Handlebars.registerHelper('sub', function (a, b) {
return parseInt(a) - parseInt(b);
})
Handlebars.registerHelper('abbrev2', function (a) {
return a.substring(0, 2);
})
Handlebars.registerHelper('abbrev3', function (a) {
return a.substring(0, 3);
})
Handlebars.registerHelper('valueAtIndex', function (arr, idx) {
return arr[idx];
})
Handlebars.registerHelper('includesKey', function (items, type, key) {
return items.filter(i => i.type === type).map(i => i.system.key).includes(key);
})
Handlebars.registerHelper('includes', function (array, val) {
return array.includes(val);
})
Handlebars.registerHelper('eval', function (expr) {
return eval(expr);
})
Handlebars.registerHelper('isOwnerOrGM', function (actor) {
console.log("Testing actor", actor.isOwner, game.userId)
return actor.isOwner || game.isGM;
})
Handlebars.registerHelper('upperFirst', function (text) {
if (typeof text !== 'string') return text
return text.charAt(0).toUpperCase() + text.slice(1)
})
Handlebars.registerHelper('upperFirstOnly', function (text) {
if (typeof text !== 'string') return text
return text.charAt(0).toUpperCase()
})
Handlebars.registerHelper('isCreature', function (key) {
return key === "creature" || key === "daemon";
})
// Handle v12 removal of this helper
Handlebars.registerHelper('select', function (selected, options) {
const escapedValue = RegExp.escape(Handlebars.escapeExpression(selected));
const rgx = new RegExp(' value=[\"\']' + escapedValue + '[\"\']');
const html = options.fn(this);
return html.replace(rgx, "$& selected");
});
}
static setupCSSRootVariables() {
const era = game.settings.get("fvtt-cthulhu-eternal", "settings-era")
let eraCSS = SYSTEM.ERA_CSS[era];
if (!eraCSS) eraCSS = SYSTEM.ERA_CSS["jazz"];
document.documentElement.style.setProperty('--font-size-standard', eraCSS.baseFontSize);
document.documentElement.style.setProperty('--font-size-title', eraCSS.titleFontSize);
document.documentElement.style.setProperty('--font-size-result', eraCSS.titleFontSize);
document.documentElement.style.setProperty('--font-primary', eraCSS.primaryFont);
document.documentElement.style.setProperty('--font-secondary', eraCSS.secondaryFont);
document.documentElement.style.setProperty('--font-title', eraCSS.titleFont);
document.documentElement.style.setProperty('--img-icon-color-filter', eraCSS.imgFilter);
document.documentElement.style.setProperty('--background-image-base', `linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.8)), url("../assets/ui/${era}_background_main.webp")`);
}
}

5967
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "fvtt-cthulhu-eternal",
"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": "UNLICENSED",
"dependencies": {
"gulp": "^5.0.0",
"gulp-less": "^5.0.0"
},
"scripts": {
"pushLDBtoYML": "node ./tools/pushLDBtoYML.mjs",
"pullYMLtoLDB": "node ./tools/pullYMLtoLDB.mjs"
},
"description": "<h2><em>Cthulhu Eternal RPG</em> for Foundry Virtual TableTop</h2>",
"main": "gulpfile.js",
"repository": {
"type": "git",
"url": "https://www.uberwald.me/gitea/uberwald/fvtt-cthulhu-eternal.git"
}
}

Binary file not shown.

View File

View File

@@ -0,0 +1 @@
MANIFEST-000140

0
packs-system/skills/LOCK Normal file
View File

8
packs-system/skills/LOG Normal file
View File

@@ -0,0 +1,8 @@
2025/02/09-18:56:19.542927 7f46adffb6c0 Recovering log #138
2025/02/09-18:56:19.597959 7f46adffb6c0 Delete type=3 #136
2025/02/09-18:56:19.598032 7f46adffb6c0 Delete type=0 #138
2025/02/09-19:16:45.192949 7f46abbff6c0 Level-0 table #143: started
2025/02/09-19:16:45.192979 7f46abbff6c0 Level-0 table #143: 0 bytes OK
2025/02/09-19:16:45.199734 7f46abbff6c0 Delete type=0 #141
2025/02/09-19:16:45.210582 7f46abbff6c0 Manual compaction at level-0 from '!folders!DD8331Hda4rhvEf9' @ 72057594037927935 : 1 .. '!items!zplzTG30QXHURusr' @ 0 : 0; will stop at (end)
2025/02/09-19:16:45.220994 7f46abbff6c0 Manual compaction at level-1 from '!folders!DD8331Hda4rhvEf9' @ 72057594037927935 : 1 .. '!items!zplzTG30QXHURusr' @ 0 : 0; will stop at (end)

View File

@@ -0,0 +1,8 @@
2025/02/09-18:43:17.595623 7f46ac7f86c0 Recovering log #134
2025/02/09-18:43:17.606503 7f46ac7f86c0 Delete type=3 #132
2025/02/09-18:43:17.606590 7f46ac7f86c0 Delete type=0 #134
2025/02/09-18:55:41.771958 7f46abbff6c0 Level-0 table #139: started
2025/02/09-18:55:41.772001 7f46abbff6c0 Level-0 table #139: 0 bytes OK
2025/02/09-18:55:41.779302 7f46abbff6c0 Delete type=0 #137
2025/02/09-18:55:41.793230 7f46abbff6c0 Manual compaction at level-0 from '!folders!DD8331Hda4rhvEf9' @ 72057594037927935 : 1 .. '!items!zplzTG30QXHURusr' @ 0 : 0; will stop at (end)
2025/02/09-18:55:41.793270 7f46abbff6c0 Manual compaction at level-1 from '!folders!DD8331Hda4rhvEf9' @ 72057594037927935 : 1 .. '!items!zplzTG30QXHURusr' @ 0 : 0; will stop at (end)

Binary file not shown.

691
styles/character.less Normal file
View File

@@ -0,0 +1,691 @@
.character-content {
.sheet-common();
.character-sheet-common();
overflow: scroll;
}
.sheet-tabs {
background-color: var(--color-light-1);
}
.character-main {
background-color: var(--color-light-1);
display: flex;
.character-pc {
display: flex;
gap: 4px;
flex: 1;
.character-left {
min-width: 180px;
display: flex;
flex-direction: column;
.character-left-image {
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 8px;
.character-img {
height: 140px;
width: auto;
border: none;
}
}
.character-hp {
gap: 2px;
align-items: center;
input {
flex: none;
width: 2.5rem;
margin-left: 2px;
margin-right: 4px;
}
.hp-separator {
font-size: calc(var(--font-size-standard) * 1.2);
display: flex;
align-items: center;
justify-content: center;
}
}
.character-dv,
.character-dmax {
.form-fields {
flex: none;
}
}
/*.character-dmax-edit {
input {
display: flex;
width: 60px;
font-size: calc(var(--font-size-standard) * 1.4);
align-items: center;
justify-content: center;
padding: 0 5px 0 5px;
text-align: center;
}
}*/
}
.character-right {
display: flex;
flex-direction: column;
gap: 5px;
.character-spec {
label {
max-width: 6rem;
}
.hero-armor {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
input {
max-width: 3rem;
}
}
}
.character-name {
display: flex;
input {
font-family: var(--font-title);
font-size: var(--font-size-title);
width: 400px;
}
}
label {
min-width: 120px;
}
}
}
.character-pc-play {
min-width: 500px;
}
.character-pc-edit {
min-width: 500px;
}
.character-skills {
background-color: var(--color-light-1);
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
.character-skill {
display: flex;
align-items: center;
.icon-skill {
width: 24px;
height: 24px;
margin-right: 4px;
}
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.rollable {
min-width: 4.5rem;
max-width: 4.5rem;
}
.char-text {
margin-left: 0.5rem;
}
.d100 {
flex: 0;
max-width: 0.6rem;
}
.form-group {
flex: 0;
padding-left: 5px;
.form-fields {
font-size: 1.1rem;
flex: none;
width: 40px;
}
}
}
}
.character-skill-play {
min-width: 225px;
}
.character-skill-edit {
min-width: 225px;
}
}
.character-biography {
background-color: var(--color-light-1);
prose-mirror.inactive {
min-height: 40px;
}
prose-mirror.active {
min-height: 150px;
}
.field-label {
margin-left: 8px;
}
.rank {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
label {
min-width: 6rem;
}
input {
max-width: 4rem;
}
}
.biodata {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
label {
min-width: 12rem;
}
}
.resources {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
label {
min-width: 8rem;
}
}
.features,
.biodata {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
label {
min-width: 3rem;
}
.feature {
display: flex;
align-items: center;
gap: 4px;
min-width: 18rem;
max-width: 18rem;
}
}
}
.tab.character-skills {
background-color: var(--color-light-1);
display: grid;
grid-template-columns: 1fr;
legend {
a {
font-size: calc(var(--font-size-standard) * 1.4);
padding-left: 5px;
}
}
.skills {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
.skill {
display: flex;
align-items: center;
gap: 4px;
margin-left: 4px;
min-width: 12.3rem;
max-width: 12.3rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.score {
min-width: 1.2rem;
max-width: 1.2rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
}
.tab.character-status {
background-color: var(--color-light-1);
display: grid;
grid-template-columns: 1fr;
legend {
a {
font-size: calc(var(--font-size-standard) * 1.4);
padding-left: 5px;
}
}
.bonds {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.bond {
display: flex;
align-items: center;
gap: 4px;
min-width: 18rem;
max-width: 18rem;
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.name {
min-width: 12rem;
max-width: 12rem;
}
.type {
min-width: 6rem;
max-width: 6rem;
}
.level {
min-width: 2rem;
max-width: 2rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.motivations {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.motivation {
display: flex;
align-items: center;
gap: 4px;
min-width: 14rem;
max-width: 14rem;
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.name {
min-width: 12rem;
max-width: 12rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.mentaldisorders {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.mentaldisorder {
display: flex;
align-items: center;
gap: 4px;
min-width: 18rem;
max-width: 18rem;
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.name {
min-width: 14rem;
max-width: 14rem;
}
.cured {
min-width: 5rem;
max-width: 5rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.injuries {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.injury {
display: flex;
align-items: center;
gap: 4px;
min-width: 16rem;
max-width: 16rem;
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.name {
min-width: 14rem;
max-width: 14rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
}
.tab.character-talents {
background-color: var(--color-light-1);
display: grid;
grid-template-columns: 1fr;
legend {
a {
font-size: calc(var(--font-size-standard) * 1.4);
padding-left: 5px;
}
}
.talents {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.talent {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.damage {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.languages {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.language {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.damage {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.psionics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.psionic {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.damage {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
}
.tab.character-equipment {
background-color: var(--color-light-1);
display: grid;
grid-template-columns: 1fr;
legend {
a {
font-size: calc(var(--font-size-standard) * 1.4);
padding-left: 5px;
}
}
.encumbrance {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
input {
max-width: 4rem;
}
.encumbered {
color: red;
font-weight: bold;
}
}
.implants {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.implant {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.damage {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.weapons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.weapon {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.damage {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.armors {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.armor {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.protection {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.equipments {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
.equipment {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
prose-mirror.inactive {
min-height: 40px;
}
prose-mirror.active {
min-height: 150px;
}
}

8
styles/chat.less Normal file
View File

@@ -0,0 +1,8 @@
&.ask-roll {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: var(--font-secondary);
font-size: calc(var(--font-size-standard) * 1.1);
}

700
styles/creature.less Normal file
View File

@@ -0,0 +1,700 @@
.creature-content {
.sheet-common();
.creature-sheet-common();
overflow: scroll;
}
.sheet-tabs {
background-color: var(--color-light-1);
}
.creature-main {
background-color: var(--color-light-1);
display: flex;
.creature-pc {
display: flex;
gap: 4px;
flex: 1;
.creature-left {
min-width: 180px;
display: flex;
flex-direction: column;
.creature-left-image {
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 8px;
.creature-img {
height: 140px;
width: auto;
border: none;
}
}
.creature-hp {
gap: 2px;
align-items: center;
input {
flex: none;
width: 2.5rem;
margin-left: 2px;
margin-right: 4px;
}
.hp-separator {
font-size: calc(var(--font-size-standard) * 1.2);
display: flex;
align-items: center;
justify-content: center;
}
}
.creature-dv,
.creature-dmax {
.form-fields {
flex: none;
}
}
.creature-dmax-edit {
input {
display: flex;
width: 60px;
font-size: calc(var(--font-size-standard) * 1.4);
align-items: center;
justify-content: center;
padding: 0 5px 0 5px;
text-align: center;
}
}
}
.creature-right {
display: flex;
flex-direction: column;
gap: 5px;
.creature-spec {
label {
max-width: 8rem;
}
select {
max-width: 10rem;
}
input {
max-width: 6rem;
}
.dice-2d6 {
max-width: 1.5rem;
}
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.rollable {
}
}
.creature-name {
display: flex;
input {
font-family: var(--font-title);
font-size: var(--font-size-title);
width: 400px;
}
}
label {
min-width: 120px;
}
}
}
.creature-pc-play {
min-width: 500px;
}
.creature-pc-edit {
min-width: 500px;
}
.creature-skills {
background-color: var(--color-light-1);
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
.creature-skill {
display: flex;
align-items: center;
.icon-skill {
width: 24px;
height: 24px;
margin-right: 4px;
}
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.rollable {
min-width: 4.5rem;
max-width: 4.5rem;
}
.char-text {
margin-left: 0.5rem;
}
.d100 {
flex: 0;
max-width: 0.6rem;
}
.form-group {
flex: 0;
padding-left: 5px;
.form-fields {
font-size: 1.1rem;
flex: none;
width: 40px;
}
}
}
}
.creature-skill-play {
min-width: 225px;
}
.creature-skill-edit {
min-width: 225px;
}
}
.creature-biography {
background-color: var(--color-light-1);
prose-mirror.inactive {
min-height: 40px;
}
prose-mirror.active {
min-height: 150px;
}
.field-label {
margin-left: 8px;
}
.rank {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
label {
min-width: 6rem;
}
input {
max-width: 4rem;
}
}
.biodata {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
label {
min-width: 12rem;
}
}
.resources {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
label {
min-width: 8rem;
}
}
.features,
.biodata {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
label {
min-width: 3rem;
}
.feature {
display: flex;
align-items: center;
gap: 4px;
min-width: 18rem;
max-width: 18rem;
}
}
}
.tab.creature-skills {
background-color: var(--color-light-1);
display: grid;
grid-template-columns: 1fr;
legend {
a {
font-size: calc(var(--font-size-standard) * 1.4);
padding-left: 5px;
}
}
.skills {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
.skill {
display: flex;
align-items: center;
gap: 4px;
margin-left: 4px;
min-width: 12.3rem;
max-width: 12.3rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.score {
min-width: 1.2rem;
max-width: 1.2rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
}
.tab.creature-status {
background-color: var(--color-light-1);
display: grid;
grid-template-columns: 1fr;
legend {
a {
font-size: calc(var(--font-size-standard) * 1.4);
padding-left: 5px;
}
}
.bonds {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.bond {
display: flex;
align-items: center;
gap: 4px;
min-width: 18rem;
max-width: 18rem;
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.name {
min-width: 12rem;
max-width: 12rem;
}
.type {
min-width: 6rem;
max-width: 6rem;
}
.level {
min-width: 2rem;
max-width: 2rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.motivations {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.motivation {
display: flex;
align-items: center;
gap: 4px;
min-width: 14rem;
max-width: 14rem;
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.name {
min-width: 12rem;
max-width: 12rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.mentaldisorders {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.mentaldisorder {
display: flex;
align-items: center;
gap: 4px;
min-width: 18rem;
max-width: 18rem;
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.name {
min-width: 14rem;
max-width: 14rem;
}
.cured {
min-width: 5rem;
max-width: 5rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.injuries {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.injury {
display: flex;
align-items: center;
gap: 4px;
min-width: 16rem;
max-width: 16rem;
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.name {
min-width: 14rem;
max-width: 14rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
}
.tab.creature-traits {
background-color: var(--color-light-1);
display: grid;
grid-template-columns: 1fr;
legend {
a {
font-size: calc(var(--font-size-standard) * 1.4);
padding-left: 5px;
}
}
.traits {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.trait {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.damage {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.abilities {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.ability {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.damage {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.psionics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.psionic {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.damage {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
}
.tab.creature-equipment {
background-color: var(--color-light-1);
display: grid;
grid-template-columns: 1fr;
legend {
a {
font-size: calc(var(--font-size-standard) * 1.4);
padding-left: 5px;
}
}
.encumbrance {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
input {
max-width: 4rem;
}
.encumbered {
color: red;
font-weight: bold;
}
}
.implants {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.implant {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.damage {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.weapons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.weapon {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.damage {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.armors {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.armor {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.protection {
min-width: 6rem;
max-width: 6rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
.equipments {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
.equipment {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13rem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
font-size: 0.7rem;
min-width: 1.8rem;
max-width: 1.8rem;
}
.name {
min-width: 10rem;
max-width: 10rem;
}
.item-img {
width: 24px;
height: 24px;
margin: 4px 0 0 0;
}
}
}
prose-mirror.inactive {
min-height: 40px;
}
prose-mirror.active {
min-height: 150px;
}
}

22
styles/equipment.less Normal file
View File

@@ -0,0 +1,22 @@
.equipment-content {
.sheet-common();
.item-sheet-common();
fieldset {
margin-top: 8px;
background-color: var(--color-light-1);
}
.header {
background-color: var(--color-light-1);
display: flex;
img {
width: 50px;
height: 50px;
}
}
label {
flex: 10%;
}
}

10
styles/fonts.less Normal file
View File

@@ -0,0 +1,10 @@
@font-face {
font-family: "Atkinson";
src: url("../assets/fonts/AtkinsonHyperlegible-Regular.ttf") format("truetype");
}
@font-face {
font-family: "Ethnocentric";
src: url("../assets/fonts/Ethnocentric-Regular.ttf") format("truetype");
}

19
styles/fvtt-hellborn.less Normal file
View File

@@ -0,0 +1,19 @@
@import "fonts.less";
@import "global.less";
.fvtt-hellborn {
@import "mixins.less";
@import "character.less";
@import "vehicle.less";
@import "creature.less";
@import "weapon.less";
@import "armor.less";
@import "equipment.less";
@import "ritual.less";
@import "perk.less";
@import "maleficias.less";
@import "species-trait.less";
@import "chat.less";
}
@import "roll.less";

80
styles/global.less Normal file
View File

@@ -0,0 +1,80 @@
:root {
--font-size-standard: 0.9rem;
--font-size-result: 1.4rem;
--background-image-base: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.8)),
url("../assets/ui/ftl_nomad_background_01.webp");
--font-primary: "Atkinson";
--font-secondary: "Atkinson";
--font-title: "Ethnocentric";
--logo-standard: url("../assets/ui/stellagama_logo_01.webp");
--color-success: rgb(15, 122, 15);
--color-failure: darkred;
--color-warning: darkorange;
--color-critical-success: rgb(21, 39, 204);
--color-critical-failure: rgb(141, 32, 231);
/*--img-icon-color-filter: invert(60%) sepia(12%) saturate(6853%) hue-rotate(81deg) brightness(113%) contrast(104%);*/
}
.d100 {
width: 18px;
height: 18px;
color: black;
border-width: 0px;
filter: var(--img-icon-color-filter);
}
.item .thumbnail,
.item-img {
/*filter: invert(90%) sepia(10%) saturate(1215%) hue-rotate(55deg) brightness(93%) contrast(89%);*/
/*filter: invert(48%) sepia(79%) saturate(2476%) hue-rotate(86deg) brightness(118%) contrast(119%);*/
filter: var(--img-icon-color-filter);
}
#logo {
content: var(--logo-standard);
width: 100px;
height: 50px;
margin-left: 15px;
}
#pause > img {
content: var(--logo-standard);
height: 192px;
width: 256px;
top: -45px;
left: calc(50% - 96px);
}
i.fvtt-ftl-nomad {
width: 36px;
height: 36px;
background-image: var(--logo-standard);
background-size: 100%;
background-position: center;
background-repeat: no-repeat;
display: flex;
position: relative;
filter: grayscale(1);
transition: 0.3s;
}
.application.dialog.fvtt-ftl-nomad {
font-family: var(--font-primary);
font-size: calc(var(--font-size-standard) * 1.0);
background-image: var(--background-image-base);
button:hover {
background: var(--color-dark-6);
}
.legend {
font-family: var(--font-primary);
}
}
.chat-message,
.chat-message.whisper {
font-family: var(--font-primary);
background-image: var(--background-image-base);
background-repeat:repeat-y;
background-position: 0%;
background-size: 100% 100%;
}

22
styles/maleficias.less Normal file
View File

@@ -0,0 +1,22 @@
.talent-content {
.sheet-common();
.item-sheet-common();
fieldset {
margin-top: 8px;
background-color: var(--color-light-1);
}
.header {
background-color: var(--color-light-1);
display: flex;
img {
width: 50px;
height: 50px;
}
}
label {
flex: 10%;
}
}

103
styles/mixins.less Normal file
View File

@@ -0,0 +1,103 @@
.sheet-common() {
font-family: var(--font-primary);
font-size: calc(var(--font-size-standard) * 1);
color: var(--color-dark-1);
background-image: var(--background-image-base);
background-repeat: no-repeat;
background-size: 100% 100%;
input:disabled,
select:disabled {
background-color: rgba(0, 0, 0, 0.2);
border-color: transparent;
color: var(--color-dark-3);
}
input,
select {
background-color: rgba(0, 0, 0, 0.1);
border-color: var(--color-dark-6);
color: var(--color-dark-2);
}
input[name="name"] {
height: 40px;
margin-right: 10px;
font-family: var(--font-title);
font-size: calc(var(--font-size-standard) * 1.2);
font-weight: bold;
border: none;
margin-top: 4px;
}
fieldset {
margin-bottom: 4px;
border-radius: 4px;
}
.form-fields {
input,
select {
text-align: center;
font-size: calc(var(--font-size-standard) * 1.0);
}
select {
font-family: var(--font-secondary);
font-size: calc(var(--font-size-standard) * 1.0);
}
}
legend {
font-family: var(--font-secondary);
font-size: calc(var(--font-size-standard) * 1.2);
font-weight: bold;
letter-spacing: 1px;
}
}
.character-sheet-common {
label {
font-family: var(--font-secondary);
font-size: calc(var(--font-size-standard) * 1.0);
}
}
.vehicle-sheet-common {
label {
font-family: var(--font-secondary);
font-size: calc(var(--font-size-standard) * 1.0);
}
}
.creature-sheet-common {
label {
font-family: var(--font-secondary);
font-size: calc(var(--font-size-standard) * 1.0);
}
}
.item-sheet-common {
.form-fields {
padding-top: 4px;
}
label {
font-family: var(--font-secondary);
font-size: calc(var(--font-size-standard) * 1.0);
flex: 50%;
}
.align-top {
align-self: flex-start;
padding: 0.1rem;
margin-right: 0.2rem;
/*border-color: black;
border-width: 1px;
border-style: solid;
border-radius: 2%;*/
}
.shift-right {
margin-left: 2rem;
}
}

22
styles/perk.less Normal file
View File

@@ -0,0 +1,22 @@
.psionic-content {
.sheet-common();
.item-sheet-common();
fieldset {
margin-top: 8px;
background-color: var(--color-light-1);
}
.header {
background-color: var(--color-light-1);
display: flex;
img {
width: 50px;
height: 50px;
}
}
label {
flex: 10%;
}
}

22
styles/ritual..less Normal file
View File

@@ -0,0 +1,22 @@
.implant-content {
.sheet-common();
.item-sheet-common();
fieldset {
margin-top: 8px;
background-color: var(--color-light-1);
}
.header {
background-color: var(--color-light-1);
display: flex;
img {
width: 50px;
height: 50px;
}
}
label {
flex: 10%;
}
}

138
styles/roll.less Normal file
View File

@@ -0,0 +1,138 @@
.application.dialog.fvtt-cthulhu-eternal {
color: var(--color-dark-1);
background-color: var(--color-light-1);
button {
background-image: none;
background-color: var(--color-dark-6);
color: var(--color-light-1);
}
input,
select {
background-color: rgba(0, 0, 0, 0.1);
border-color: var(--color-dark-6);
color: var(--color-dark-2);
}
}
.fvtt-cthulhu-eternal-roll-dialog {
fieldset {
padding: 10px;
background-color: var(--color-light-1);
}
}
.dialog-modifier {
display: flex;
justify-content: center;
align-items: center;
select {
border: none;
background-color: rgba(0, 0, 0, 0.1);
color: var(--color-dark-2);
width: 10rem;
text-align: center;
}
}
.red-warning {
color: var(--color-failure);
}
.orange-warning {
color: var(--color-warning);
}
.dialog-damage {
display: flex;
justify-content: center;
align-items: center;
font-family: var(--font-secondary);
font-size: calc(var(--font-size-standard) * 2);
color: var(--color-dark-1);
}
&.dice-roll {
flex-direction: column;
.dice-total,
.dice-formula {
padding-top: 5px;
}
.dice-total {
margin-bottom: 5px;
}
.message-header {
font-family: var(--font-primary);
}
img {
border: 0px;
}
.intro-chat {
color:var(--color-dark-1);
border-radius: 20px;
display: flex;
flex-direction: row;
.intro-img {
padding: 5px;
width: 80px;
align-self: center;
}
.intro-right {
display: flex;
flex-direction: column;
ul {
list-style-type: none;
padding: 0;
margin: 0;
justify-content: center;
align-items: center;
li {
margin: 0 10px;
font-family: var(--font-primary);
font-size: calc(var(--font-size-standard) * 1.0);
}
.nudge-roll {
font-size: calc(var(--font-size-standard) * 1.0);
margin-left: 4rem;
display: none;
}
.result-success {
color: var(--color-success);
font-family: var(--font-title);
font-size: var(--font-size-result);
}
.result-critical-success {
color: var(--color-critical-success);
font-family: var(--font-title);
font-size: var(--font-size-result);
}
.result-failure {
color: var(--color-failure);
font-family: var(--font-title);
font-size: var(--font-size-result);
}
.result-critical-failure {
color: var(--color-critical-failure);
font-family: var(--font-title);
font-size: var(--font-size-result);
}
}
.introText {
font-family: var(--font-secondary);
font-size: calc(var(--font-size-standard) * 1.2);
width: 210px;
margin-left: 20px;
}
}
}
.result {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: calc(var(--font-size-standard) * 1.2);
text-shadow: 0 0 10px var(--color-shadow-primary);
}
}

22
styles/species-trait.less Normal file
View File

@@ -0,0 +1,22 @@
.creature-trait-content {
.sheet-common();
.item-sheet-common();
fieldset {
margin-top: 8px;
background-color: var(--color-light-1);
}
.header {
background-color: var(--color-light-1);
display: flex;
img {
width: 50px;
height: 50px;
}
}
label {
flex: 10%;
}
}

235
styles/vehicle.less Normal file
View File

@@ -0,0 +1,235 @@
.vehicle-content {
.sheet-common();
.vehicle-sheet-common();
overflow: scroll;
}
.sheet-tabs {
background-color: var(--color-light-1);
}
.vehicle-main {
background-color: var(--color-light-1);
display: flex;
.vehicle-pc {
display: flex;
gap: 4px;
flex: 1;
.vehicle-left {
min-width: 180px;
display: flex;
flex-direction: column;
.vehicle-left-image {
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 8px;
.vehicle-img {
height: 140px;
width: auto;
border: none;
}
}
}
.vehicle-right {
display: flex;
flex-direction: column;
gap: 5px;
.vehicle-name {
display: flex;
input {
font-family: var(--font-title);
font-size: calc(var(--font-size-standard) * 1.4);
width: 400px;
}
}
.cargo,
.capacity {
label {
max-width: 5rem;
}
input {
max-width: 3.5rem;
margin-right: 0.5rem;
}
}
.vehicle-infos {
display: flex;
flex-direction: column;
gap: 4px;
label {
min-width: 120px;
}
.vehicle-hp {
display: flex;
gap: 2px;
align-items: center;
.vehicle-hp-value {
.form-fields input {
flex: none;
width: 50px;
margin-left: 4px;
font-size: calc(var(--font-size-standard) * 1.4);
}
}
.vehicle-hp-max {
clear: both;
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 3px 0;
align-items: center;
input {
width: 50px;
text-align: center;
font-size: calc(var(--font-size-standard) * 1.4);
}
}
.hp-separator {
font-size: calc(var(--font-size-standard) * 1.2);
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
.vehicle-pc-play {
min-width: 500px;
}
.vehicle-pc-edit {
min-width: 500px;
}
}
.vehicle-description {
background-color: var(--color-light-1);
prose-mirror.inactive {
min-height: 40px;
}
prose-mirror.active {
min-height: 150px;
}
.field-label {
margin-left: 8px;
}
.biodata {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
label {
min-width: 3.0rem;
}
.feature {
display: flex;
align-items: center;
gap: 4px;
min-width: 18rem;
max-width: 18rem;
}
}
}
.tab.vehicle-equipment {
background-color: var(--color-light-1);
display: grid;
grid-template-columns: 1fr;
legend {
a {
font-size: calc(var(--font-size-standard) * 1.4);
padding-left: 5px;
}
}
.weapons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
.weapon {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13srem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
min-width: 2rem;
max-width: 2rem;
}
.damage {
min-width: 5rem;
max-width: 5rem;
}
.name {
min-width: 8rem;
max-width: 8rem;
}
.item-img {
width: 32px;
height: 32px;
margin: 4px 0 0 0;
}
}
}
.equipments {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
.equipment {
display: flex;
align-items: center;
gap: 4px;
min-width: 13rem;
max-width: 13srem;
.rollable:hover,
.rollable:focus {
text-shadow: 0 0 8px var(--color-shadow-primary);
cursor: pointer;
}
.controls {
min-width: 2rem;
max-width: 2rem;
}
.damage {
min-width: 5rem;
max-width: 5rem;
}
.name {
min-width: 8rem;
max-width: 8rem;
}
.item-img {
width: 32px;
height: 32px;
margin: 4px 0 0 0;
}
}
}
prose-mirror.inactive {
min-height: 40px;
}
prose-mirror.active {
min-height: 150px;
}
}

22
styles/weapon.less Normal file
View File

@@ -0,0 +1,22 @@
.weapon-content {
.sheet-common();
.item-sheet-common();
.header {
background-color: var(--color-light-1);
display: flex;
img {
width: 50px;
height: 50px;
}
}
fieldset {
margin-top: 8px;
background-color: var(--color-light-1);
}
label {
flex: 10%;
}
}

59
system.json Normal file
View File

@@ -0,0 +1,59 @@
{
"id": "fvtt-hellborn",
"title": "Hellborn -Descendd RPG",
"description": "Hellborn -Descendd RPG",
"manifest": "https://www.uberwald.me/gitea/public/fvtt-hellborn/raw/branch/main/system.json",
"download": "#{DOWNLOAD}#",
"url": "https://www.uberwald.me/gitea/public/fvtt-hellborn",
"license": "LICENSE",
"version": "12.0.0",
"authors": [
{
"name": "Uberwald",
"discord": "LeRatierBretonnien"
}
],
"flags": {
"hotReload": {
"extensions": ["css", "html", "hbs", "json"],
"paths": ["acks.css", "./", "templates", "css", "lang/en.json"]
}
},
"compatibility": {
"minimum": "12",
"verified": "12"
},
"esmodules": ["fvtt-hellborn.mjs"],
"styles": ["css/fvtt-hellborn.css"],
"languages": [
{
"lang": "en",
"name": "Anglais",
"path": "lang/en.json"
}
],
"documentTypes": {
"Actor": {
"character": { "htmlFields": ["description", "notes"] },
"monster": { "htmlFields": ["description", "notes"] },
"vehicle": { "htmlFields": ["description", "notes"] }
},
"Item": {
"perk": { "htmlFields": ["description"] },
"weapon": { "htmlFields": ["description"] },
"equipment": { "htmlFields": ["description"] },
"maleficias": { "htmlFields": ["description"] },
"species-trait": { "htmlFields": ["description"] },
"ritual": { "htmlFields": ["description"] }
}
},
"packs": [
],
"grid": {
"distance": 10,
"units": "m"
},
"primaryTokenAttribute": "hp",
"socket": true,
"background": "systems/fvtt-hellborn/assets/ui/ftl_nomad_background_01.webp"
}

20
templates/armor.hbs Normal file
View File

@@ -0,0 +1,20 @@
<section>
<div class="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>
<fieldset>
{{formField systemFields.techAge value=system.techAge localize=true}}
{{formField systemFields.protection value=system.protection}}
{{formField systemFields.enc value=system.enc}}
{{formField systemFields.cost value=system.cost}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description"
toggled=true}}
</fieldset>
</section>

View File

@@ -0,0 +1,34 @@
<section class="tab character-{{tab.id}} {{tab.cssClass}}" data-tab="{{tab.id}}" data-group="{{tab.group}}">
<fieldset class="rank">
<legend>Rank</legend>
{{formField systemFields.rank.fields.experienced value=system.rank.experienced type="number" rootId=partId disabled=isPlayMode}}
{{formField systemFields.rank.fields.expert value=system.rank.expert type="number" rootId=partId disabled=isPlayMode}}
{{formField systemFields.rank.fields.veteran value=system.rank.veteran type="number" rootId=partId disabled=isPlayMode}}
{{formField systemFields.rank.fields.elite value=system.rank.elite type="number" rootId=partId disabled=isPlayMode}}
{{formField systemFields.rank.fields.legend value=system.rank.legend type="number" rootId=partId disabled=isPlayMode}}
</fieldset>
<fieldset class="biodata">
<legend>{{localize "FTLNOMAD.Label.biodata"}}</legend>
{{formField systemFields.biodata.fields.gender value=system.biodata.gender rootId=partId disabled=isPlayMode classes="short"}}
{{formField systemFields.biodata.fields.age value=system.biodata.age rootId=partId disabled=isPlayMode classes="short"}}
{{formField systemFields.biodata.fields.height value=system.biodata.height rootId=partId disabled=isPlayMode classes="short"}}
{{formField systemFields.biodata.fields.weight value=system.biodata.weight rootId=partId disabled=isPlayMode classes="short"}}
{{formField systemFields.biodata.fields.eyes value=system.biodata.eyes rootId=partId disabled=isPlayMode classes="short"}}
{{formField systemFields.biodata.fields.hair value=system.biodata.hair rootId=partId disabled=isPlayMode classes="short"}}
{{formField systemFields.biodata.fields.home value=system.biodata.home rootId=partId disabled=isPlayMode}}
{{formField systemFields.biodata.fields.birthplace value=system.biodata.birthplace rootId=partId disabled=isPlayMode}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.notes"}}</legend>
{{formInput systemFields.notes enriched=enrichedNotes value=system.notes name="system.notes" toggled=true}}
</fieldset>
</section>

View File

@@ -0,0 +1,126 @@
<section class="tab character-{{tab.id}} {{tab.cssClass}}" data-tab="{{tab.id}}" data-group="{{tab.group}}">
<fieldset class="encumbrance">
{{#if isEncumbered}}
{{formField systemFields.enc.fields.value value=system.enc.value rootId=partId disabled=true classes="encumbered"}}
{{else }}
{{formField systemFields.enc.fields.value value=system.enc.value rootId=partId disabled=true}}
{{/if}}
{{formField systemFields.enc.fields.max value=system.enc.max rootId=partId disabled=isPlayMode}}
{{formField systemFields.credits value=system.credits rootId=partId }}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.weapons"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addWeapon"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createWeapon"></i></a>{{/if}}
</legend>
<div class="weapons">
{{#each weapons as |item|}}
<div class="weapon item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<i class="fa-regular fa-dice"></i>
<div class="name rollable" data-roll-type="weapon" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<i class="fa-regular fa-dice"></i>
<a class="damage rollable" data-item-id="{{item.id}}" data-action="roll" data-roll-type="damage"
data-roll-value="{{item.system.damage}}">
{{localize "FTLNOMAD.Label.damageShort"}} :
{{item.system.damage}}</a>
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.armors"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addArmor"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createArmor"></i></a>{{/if}}
</legend>
<div class="armors">
{{#each armors as |item|}}
<div class="armor item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<div class="name" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<span class="protection">{{localize "FTLNOMAD.Label.armor"}} : {{item.system.protection}}</span>
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.implants"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addTalent"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createImplant"></i></a>{{/if}}
</legend>
<div class="implants">
{{#each implants as |item|}}
{{!log 'weapon' this}}
<div class="implant item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<img src="systems/fvtt-cthulhu-eternal/assets/ui/d100.svg" class="d100" />
<div class="name" data-roll-type="weapon" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<img src="systems/fvtt-cthulhu-eternal/assets/ui/d100.svg" class="d100" />
<a class="damage rollable" data-item-id="{{item.id}}" data-action="roll" data-roll-type="damage"
data-roll-value="{{item.system.damage}}">
{{#if item.system.isAdvantage}}
<i data-tooltip="Provides advantage" class="fas fa-circle-chevron-up"></i>
{{else}}
{{/if}}
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.equipments"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addEquipment"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createEquipment"></i></a>{{/if}}
</legend>
<div class="equipments">
{{#each equipments as |item|}}
{{!log 'armor' this}}
<div class="equipment" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<div class="name" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
</section>

View File

@@ -0,0 +1,99 @@
<section class="character-main character-main-{{ifThen isPlayMode 'play' 'edit'}}">
{{!log "character-main" this}}
<fieldset>
<legend>{{localize "FTLNOMAD.Label.character"}}</legend>
<div class="character-pc character-pc-{{ifThen isPlayMode 'play' 'edit'}}">
<div class="character-left">
<div class="character-left-image">
<img class="character-img" src="{{actor.img}}" data-edit="img" data-action="editImage"
data-tooltip="{{actor.name}}" />
</div>
<fieldset class="character-hp">
<legend>{{localize "FTLNOMAD.Label.Stamina"}}</legend>
<div class="flexrow">
{{formField systemFields.health.fields.staminaValue value=system.health.staminaValue}}
{{formField systemFields.health.fields.staminaMax value=system.health.staminaMax rootId=partId disabled=true}}
</div>
<div class="flexrow">
{{formField systemFields.health.fields.wounds value=system.health.wounds }}
</div>
</fieldset>
</div>
<div class="character-right">
<div class="character-name">
{{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}}
<a class="control" data-action="toggleSheet" data-tooltip="FTLNOMAD.ToggleSheet" data-tooltip-direction="UP">
<i class="fa-solid fa-user-{{ifThen isPlayMode 'lock' 'pen'}}"></i>
</a>
</div>
<fieldset class="character-spec">
{{formField systemFields.concept value=system.concept rootId=partId disabled=isPlayMode}}
{{formField systemFields.species value=system.species rootId=partId disabled=isPlayMode}}
{{formField systemFields.archetype value=system.archetype rootId=partId disabled=isPlayMode}}
<div class="hero-armor">
{{formField systemFields.heroPoints value=system.heroPoints rootId=partId }}
{{formField systemFields.armor.fields.value value=system.armor.value rootId=partId disabled=isPlayMode}}
</div>
</fieldset>
</div>
</div>
</fieldset>
<fieldset class="character-skills character-skills-{{ifThen isPlayMode 'play' 'edit'}}">
<legend>{{localize "FTLNOMAD.Label.skills"}}</legend>
<div class="character-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_combat.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="combat">{{localize
"FTLNOMAD.Label.combat"}}</label>
{{formInput systemFields.skills.fields.combat.fields.value value=system.skills.combat.value rootId=partId disabled=isPlayMode type="number" }}
</div>
<div class="character-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_knowledge.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="knowledge">{{localize
"FTLNOMAD.Label.knowledge"}}</label>
{{formInput systemFields.skills.fields.knowledge.fields.value value=system.skills.knowledge.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
<div class="character-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_social.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="social">{{localize
"FTLNOMAD.Label.social"}}</label>
{{formInput systemFields.skills.fields.social.fields.value value=system.skills.social.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
<div class="character-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_physical.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="physical">{{localize
"FTLNOMAD.Label.physical"}}</label>
{{formInput systemFields.skills.fields.physical.fields.value value=system.skills.physical.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
<div class="character-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_stealth.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="stealth">{{localize
"FTLNOMAD.Label.stealth"}}</label>
{{formInput systemFields.skills.fields.stealth.fields.value value=system.skills.stealth.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
<div class="character-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_vehicles.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="vehicles">{{localize
"FTLNOMAD.Label.vehicles"}}</label>
{{formInput systemFields.skills.fields.vehicles.fields.value value=system.skills.vehicles.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
<div class="character-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_technology.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="technology">{{localize
"FTLNOMAD.Label.technology"}}</label>
{{formInput systemFields.skills.fields.technology.fields.value value=system.skills.technology.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
</fieldset>
</section>

View File

@@ -0,0 +1,106 @@
<section class="tab character-{{tab.id}} {{tab.cssClass}}" data-tab="{{tab.id}}" data-group="{{tab.group}}">
<fieldset>
<legend>{{localize "FTLNOMAD.Label.talents"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addTalent"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createTalent"></i></a>{{/if}}
</legend>
<div class="talents">
{{#each talents as |item|}}
{{!log 'weapon' this}}
<div class="talent item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<img src="systems/fvtt-cthulhu-eternal/assets/ui/d100.svg" class="d100" />
<div class="name" data-roll-type="weapon" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<img src="systems/fvtt-cthulhu-eternal/assets/ui/d100.svg" class="d100" />
<a class="damage rollable" data-item-id="{{item.id}}" data-action="roll" data-roll-type="damage"
data-roll-value="{{item.system.damage}}">
{{#if item.system.isAdvantage}}
<i data-tooltip="Provides advantage" class="fas fa-circle-chevron-up"></i>
{{else}}
{{/if}}
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.psionics"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addImplant"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createPsionic"></i></a>{{/if}}
</legend>
<div class="psionics">
{{#each psionics as |item|}}
{{!log 'weapon' this}}
<div class="psionic item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<img src="systems/fvtt-cthulhu-eternal/assets/ui/d100.svg" class="d100" />
<div class="name" data-roll-type="weapon" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<img src="systems/fvtt-cthulhu-eternal/assets/ui/d100.svg" class="d100" />
<a class="damage rollable" data-item-id="{{item.id}}" data-action="roll" data-roll-type="damage"
data-roll-value="{{item.system.damage}}">
{{#if item.system.isAdvantage}}
<i data-tooltip="Provides advantage" class="fas fa-circle-chevron-up"></i>
{{else}}
{{/if}}
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.languages"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addLanguage"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createLanguage"></i></a>{{/if}}
</legend>
<div class="languages">
{{#each languages as |item|}}
{{!log 'weapon' this}}
<div class="language item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<img src="systems/fvtt-cthulhu-eternal/assets/ui/d100.svg" class="d100" />
<div class="name" data-roll-type="weapon" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<img src="systems/fvtt-cthulhu-eternal/assets/ui/d100.svg" class="d100" />
<a class="damage rollable" data-item-id="{{item.id}}" data-action="roll" data-roll-type="damage"
data-roll-value="{{item.system.damage}}">
{{#if item.system.isAdvantage}}
<i data-tooltip="Provides advantage" class="fas fa-circle-chevron-up"></i>
{{else}}
{{/if}}
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
</section>

View File

@@ -0,0 +1,57 @@
{{!log 'chat-message' this}}
<div class="{{cssClass}}">
<div class="intro-chat">
<div class="intro-img">
<img src="{{actingCharImg}}" data-tooltip="{{actingCharName}}" />
</div>
<div class="intro-right">
<ul>
{{#if (eq rollType "skill")}}
<li><strong>{{localize "FTLNOMAD.Label.skillRoll"}}</strong></li>
{{/if}}
{{#if weapon}}
<li><strong>Weapon : {{weapon.name}}</strong></li>
{{/if}}
<li><strong>{{localize rollItem.label}} : {{fullFormula}}</strong></li>
{{#if isEncumbered}}
<li class="red-warning">Encumbered : -1D</li>
{{/if}}
<li>{{localize "FTLNOMAD.Label.modifier"}} : {{numericModifier}}D</li>
{{#if isSuccess}}
<li class="result-success">
{{localize "FTLNOMAD.Label.success"}}
</li>
{{/if}}
{{#if isFailure}}
<li class="result-failure">
{{localize "FTLNOMAD.Label.failure"}}
</li>
{{/if}}
</ul>
</div>
</div>
{{#if isDamage}}
<div>
{{#if (and isGM hasTarget)}}
{{{localize "FTLNOMAD.Roll.displayArmor" targetName=targetName targetArmor=targetArmor
realDamage=realDamage}}}
{{/if}}
</div>
{{/if}}
{{#unless isPrivate}}
<div class="dice-result">
<h4 class="dice-total">{{total}}</h4>
<div class="dice-formula">{{formula}}</div>
{{{tooltip}}}
</div>
{{/unless}}
</div>

View File

@@ -0,0 +1,17 @@
<section>
<div class="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>
<fieldset>
{{formField systemFields.isAdvantage value=system.isAdvantage}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description"
toggled=true}}
</fieldset>
</section>

View File

@@ -0,0 +1,14 @@
<section class="tab creature-{{tab.id}} {{tab.cssClass}}" data-tab="{{tab.id}}" data-group="{{tab.group}}">
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description"
toggled=true}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.notes"}}</legend>
{{formInput systemFields.notes enriched=enrichedNotes value=system.notes name="system.notes" toggled=true}}
</fieldset>
</section>

103
templates/creature-main.hbs Normal file
View File

@@ -0,0 +1,103 @@
<section class="creature-main creature-main-{{ifThen isPlayMode 'play' 'edit'}}">
{{!log "creature-main" this}}
<fieldset>
<legend>{{localize "FTLNOMAD.Label.creature"}}</legend>
<div class="creature-pc creature-pc-{{ifThen isPlayMode 'play' 'edit'}}">
<div class="creature-left">
<div class="creature-left-image">
<img class="creature-img" src="{{actor.img}}" data-edit="img" data-action="editImage"
data-tooltip="{{actor.name}}" />
</div>
<fieldset class="creature-hp">
<legend>{{localize "FTLNOMAD.Label.Stamina"}}</legend>
<div class="flexrow">
Curr. {{formField systemFields.health.fields.staminaValue value=system.health.staminaValue}}
Max {{formField systemFields.health.fields.staminaMax value=system.health.staminaMax rootId=partId}}
</div>
</fieldset>
</div>
<div class="creature-right">
<div class="creature-name">
{{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}}
<a class="control" data-action="toggleSheet" data-tooltip="FTLNOMAD.ToggleSheet" data-tooltip-direction="UP">
<i class="fa-solid fa-user-{{ifThen isPlayMode 'lock' 'pen'}}"></i>
</a>
</div>
<fieldset class="creature-spec">
<legend>{{localize "FTLNOMAD.Label.Details"}}</legend>
{{formField systemFields.terrain value=system.terrain rootId=partId disabled=isPlayMode localize=true}}
{{formField systemFields.niche value=system.niche rootId=partId disabled=isPlayMode localize=true}}
{{formField systemFields.size value=system.size rootId=partId disabled=isPlayMode localize=true}}
<div class="flexrow rollable" data-roll-type="creature-number">
<i class="fa-regular fa-dice dice-2d6"></i>
{{formField systemFields.numberAppearing value=system.numberAppearing rootId=partId disabled=isPlayMode
localize=true}}
</div>
<div class="flexrow rollable" data-roll-type="creature-damage">
<i class="fa-regular fa-dice dice-2d6"></i>
{{formField systemFields.damage value=system.damage rootId=partId disabled=isPlayMode localize=true}}
</div>
</fieldset>
</div>
</div>
</fieldset>
<fieldset class="creature-skills creature-skills-{{ifThen isPlayMode 'play' 'edit'}}">
<legend>{{localize "FTLNOMAD.Label.skills"}}</legend>
<div class="creature-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_combat.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="combat">{{localize
"FTLNOMAD.Label.combat"}}</label>
{{formInput systemFields.skills.fields.combat.fields.value value=system.skills.combat.value rootId=partId
disabled=isPlayMode type="number" }}
</div>
<div class="creature-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_knowledge.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="knowledge">{{localize
"FTLNOMAD.Label.knowledge"}}</label>
{{formInput systemFields.skills.fields.knowledge.fields.value value=system.skills.knowledge.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
<div class="creature-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_social.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="social">{{localize
"FTLNOMAD.Label.social"}}</label>
{{formInput systemFields.skills.fields.social.fields.value value=system.skills.social.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
<div class="creature-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_physical.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="physical">{{localize
"FTLNOMAD.Label.physical"}}</label>
{{formInput systemFields.skills.fields.physical.fields.value value=system.skills.physical.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
<div class="creature-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_stealth.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="stealth">{{localize
"FTLNOMAD.Label.stealth"}}</label>
{{formInput systemFields.skills.fields.stealth.fields.value value=system.skills.stealth.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
<div class="creature-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_vehicles.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="vehicles">{{localize
"FTLNOMAD.Label.vehicles"}}</label>
{{formInput systemFields.skills.fields.vehicles.fields.value value=system.skills.vehicles.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
<div class="creature-skill">
<img src="systems/fvtt-ftl-nomad/assets/icons/icon_technology.svg" class="icon-skill" />
<label class="rollable" data-roll-type="skill" data-skill-id="technology">{{localize
"FTLNOMAD.Label.technology"}}</label>
{{formInput systemFields.skills.fields.technology.fields.value value=system.skills.technology.value
rootId=partId disabled=isPlayMode type="number"}}
</div>
</fieldset>
</section>

View File

@@ -0,0 +1,50 @@
<section class="tab creature-{{tab.id}} {{tab.cssClass}}" data-tab="{{tab.id}}" data-group="{{tab.group}}">
<fieldset>
<legend>{{localize "FTLNOMAD.Label.traits"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addTrait"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createTrait"></i></a>{{/if}}
</legend>
<div class="traits">
{{#each traits as |item|}}
<div class="trait item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<div class="name" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.abilities"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addAbility"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createAbility"></i></a>{{/if}}
</legend>
<div class="abilities">
{{#each abilities as |item|}}
{{!log 'armor' this}}
<div class="ability" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<div class="name" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
</section>

View File

@@ -0,0 +1,17 @@
<section>
<div class="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>
<fieldset>
{{formField systemFields.isAdvantage value=system.isAdvantage}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description"
toggled=true}}
</fieldset>
</section>

19
templates/equipment.hbs Normal file
View File

@@ -0,0 +1,19 @@
<section>
<div class="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>
<fieldset>
{{formField systemFields.techAge value=system.techAge localize=true}}
{{formField systemFields.enc value=system.enc}}
{{formField systemFields.cost value=system.cost}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=description value=system.description name="system.description"
toggled=true}}
</fieldset>
</section>

19
templates/implant.hbs Normal file
View File

@@ -0,0 +1,19 @@
<section>
<div class="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>
<fieldset>
{{formField systemFields.techAge value=system.techAge localize=true}}
{{formField systemFields.enc value=system.enc}}
{{formField systemFields.cost value=system.cost}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=description value=system.description name="system.description"
toggled=true}}
</fieldset>
</section>

12
templates/language.hbs Normal file
View File

@@ -0,0 +1,12 @@
<section>
<div class="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>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=description value=system.description name="system.description" toggled=true}}
</fieldset>
</section>h

12
templates/psionic.hbs Normal file
View File

@@ -0,0 +1,12 @@
<section>
<div class="header">
<img class="item-img era-icon-color" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=description value=system.description name="system.description" toggled=true}}
</fieldset>
</section>

59
templates/roll-dialog.hbs Normal file
View File

@@ -0,0 +1,59 @@
<div class="fvtt-ftl-nomad-roll-dialog">
<fieldSet>
{{#if (eq rollType "skill")}}
<legend>{{localize "FTLNOMAD.Label.skill"}}</legend>
{{/if}}
<div class="dialog-skill">{{localize rollItem.label}} : 2d6+{{rollItem.value}}</div>
{{#if weapon}}
<div class="dialog-skill">Weapon : {{weapon.name}}</div>
{{/if}}
{{#if isEncumbered}}
<div class="dialog-skill red-warning">Encumbered : -1D</div>
{{/if}}
</fieldSet>
<fieldSet class="dialog-modifier">
<legend>{{localize "FTLNOMAD.Label.modifier"}}</legend>
<select name="modifier" class="roll-skill-modifier">
{{selectOptions choiceModifier selected=modifier localize=true}}
</select>
{{#if weapon}}
<select name="range-modifier" class="roll-skill-range-modifier">
{{selectOptions choiceRangeModifier selected=rangeModifier}}
</select>
<ul>
<li>Two Attacks : <input type="checkbox" name="isAiming" data-field="two-attacks" class="select-combat-option"></li>
<li>Aiming : <input type="checkbox" data-field="aiming" class="select-combat-option"></li>
<li>Dim Lightning : <input type="checkbox" data-field="dim" class="select-combat-option"></li>
<li>Darkness : <input type="checkbox" data-field="darkness" class="select-combat-option"></li>
<li>Target Prone/Obscured : <input type="checkbox" data-field="prone" class="select-combat-option"></li>
<li>Target Cover : <input type="checkbox" data-field="cover" class="select-combat-option"></li>
<li>1/2 Auto Fire Recoil : <input type="checkbox" data-field="recoil-first" class="select-combat-option"></li>
<li>2+ Auto Fire Recoil : <input type="checkbox" data-field="recoil-third" class="select-combat-option"></li>
<li>Target Aware : <input type="checkbox" data-field="aware" class="select-combat-option"></li>
</ul>
{{/if}}
</fieldSet>
<fieldSet class="dialog-formula">
<legend>{{localize "FTLNOMAD.Label.formula"}}</legend>
<label name="fullFormula" id="roll-dialog-full-formula" >{{fullFormula}}</label>
</fieldSet>
<fieldSet>
<legend>{{localize "FTLNOMAD.Label.rollView"}}</legend>
<select name="visibility">
{{selectOptions rollModes selected=visibility}}
</select>
</fieldSet>
</div>

View File

@@ -0,0 +1,24 @@
<section class="tab starship-{{tab.id}} {{tab.cssClass}}" data-tab="{{tab.id}}" data-group="{{tab.group}}">
<fieldset>
<legend>{{localize "FTLNOMAD.Label.damages"}}</legend>
<textarea class="form-control" rows="5" name="system.damages"
data-tooltip="{{localize "FTLNOMAD.Tooltip.damages"}}">{{system.damages}}</textarea>
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.modifications"}}</legend>
{{formInput systemFields.modifications enriched=enrichedModifications value=system.modifications name="system.modifications" toggled=true}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.notes"}}</legend>
{{formInput systemFields.notes enriched=enrichedNotes value=system.notes name="system.notes" toggled=true}}
</fieldset>
</section>

View File

@@ -0,0 +1,58 @@
<section class="starship-main starship-main-{{ifThen isPlayMode 'play' 'edit'}}">
<fieldset>
<legend>{{localize "FTLNOMAD.Label.starship"}}</legend>
<div class="starship-pc starship-pc-{{ifThen isPlayMode 'play' 'edit'}}">
<div class="starship-left">
<div class="starship-left-image">
<img class="starship-img" src="{{actor.img}}" data-edit="img" data-action="editImage"
data-tooltip="{{actor.name}}" />
</div>
<fieldset class="spec">
{{formField systemFields.agility value=system.agility localize=true}}
{{formField systemFields.armor value=system.armor localize=true}}
{{formField systemFields.endurance value=system.endurance localize=true}}
</fieldset>
</div>
<div class="starship-right">
<div class="starship-name">
{{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}}
<a class="control" data-action="toggleSheet" data-tooltip="FTLNOMAD.ToggleSheet"
data-tooltip-direction="UP">
<i class="fa-solid fa-user-{{ifThen isPlayMode 'lock' 'pen'}}"></i>
</a>
</div>
<fieldset class="capacity ">
<legend>{{localize "FTLNOMAD.Label.capacity"}}</legend>
<div class="flexrow">
{{formField systemFields.hullType value=system.hullType localize=true}}
<div class="flexrow rollable" data-roll-type="starship-guns">
<i class="fa-regular fa-dice dice-2d6"></i>
{{formField systemFields.guns value=system.guns localize=true}}
</div>
</div>
<div class="flexrow">
{{formField systemFields.cost value=system.cost localize=true}}
{{formField systemFields.monthlyCost value=system.monthlyCost localize=true}}
</div>
<div class="flexrow">
{{formField systemFields.travelMultiplier value=system.travelMultiplier localize=true}}
</div>
</fieldset>
<fieldset class="cargo">
<legend>{{localize "FTLNOMAD.Label.cargo"}}</legend>
<div class="flexrow">
{{formField systemFields.crew value=system.crew localize=true}}
{{formField systemFields.cargo value=system.cargo localize=true}}
</div>
</fieldset>
</div>
</div>
</fieldset>
</section>

17
templates/talent.hbs Normal file
View File

@@ -0,0 +1,17 @@
<section>
<div class="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>
<fieldset>
{{formField systemFields.isAdvantage value=system.isAdvantage}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description"
toggled=true}}
</fieldset>
</section>

View File

@@ -0,0 +1,19 @@
<section class="tab vehicle-{{tab.id}} {{tab.cssClass}}" data-tab="{{tab.id}}" data-group="{{tab.group}}">
<fieldset>
<legend>{{localize "FTLNOMAD.Label.damages"}}</legend>
<textarea class="form-control" rows="5" name="system.damages"
data-tooltip="{{localize "FTLNOMAD.Tooltip.damages"}}">{{system.damages}}</textarea>
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.notes"}}</legend>
{{formInput systemFields.notes enriched=enrichedNotes value=system.notes name="system.notes" toggled=true}}
</fieldset>
</section>

View File

@@ -0,0 +1,54 @@
<section class="tab vehicle-{{tab.id}} {{tab.cssClass}}" data-tab="{{tab.id}}" data-group="{{tab.group}}">
<fieldset>
<legend>{{localize "FTLNOMAD.Label.weapons"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addWeapon"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createWeapon"></i></a>{{/if}}
</legend>
<div class="weapons">
{{#each weapons as |item|}}
{{!log 'weapon' this}}
<div class="weapon item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<div class="name rollable" data-roll-type="weapon" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<a class="damage rollable" data-item-id="{{item.id}}" data-action="roll" data-roll-type="damage"
data-roll-value="{{item.system.damage}}">{{localize "FTLNOMAD.Label.damageShort"}} :
{{item.system.damage}}</a>
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.equipment"}}{{#if isEditMode}}
<a class="action" data-tooltip="{{localize "FTLNOMAD.Tooltip.addEquipment"}}" data-tooltip-direction="UP"><i
class="fas fa-plus" data-action="createEquipment"></i></a>{{/if}}
</legend>
<div class="equipments">
{{#each equipments as |item|}}
{{!log 'armor' this}}
<div class="equipment" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<div class="name" data-tooltip="{{{item.system.description}}}">
{{item.name}}
</div>
<div class="controls">
<a data-tooltip="{{localize 'FTLNOMAD.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
<a data-tooltip="{{localize 'FTLNOMAD.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div>
</div>
{{/each}}
</div>
</fieldset>
</section>

View File

@@ -0,0 +1,54 @@
<section class="vehicle-main vehicle-main-{{ifThen isPlayMode 'play' 'edit'}}">
<fieldset>
<legend>{{localize "FTLNOMAD.Label.vehicle"}}</legend>
<div class="vehicle-pc vehicle-pc-{{ifThen isPlayMode 'play' 'edit'}}">
<div class="vehicle-left">
<div class="vehicle-left-image">
<img class="vehicle-img" src="{{actor.img}}" data-edit="img" data-action="editImage"
data-tooltip="{{actor.name}}" />
</div>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.Agility"}}</legend>
<div class="flexrow">
{{formField systemFields.agility value=system.agility localize=true}}
</div>
</fieldset>
</div>
<div class="vehicle-right">
<div class="vehicle-name">
{{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}}
<a class="control" data-action="toggleSheet" data-tooltip="FTLNOMAD.ToggleSheet"
data-tooltip-direction="UP">
<i class="fa-solid fa-user-{{ifThen isPlayMode 'lock' 'pen'}}"></i>
</a>
</div>
<fieldset class="capacity ">
<legend>{{localize "FTLNOMAD.Label.capacity"}}</legend>
<div class="flexrow">
{{formField systemFields.armor value=system.armor localize=true}}
{{formField systemFields.force value=system.force localize=true}}
</div>
<div class="flexrow">
{{formField systemFields.range value=system.range localize=true}}
{{formField systemFields.speed value=system.speed localize=true}}
</div>
</fieldset>
<fieldset class="cargo">
<legend>{{localize "FTLNOMAD.Label.cargo"}}</legend>
<div class="flexrow">
{{formField systemFields.crew value=system.crew localize=true}}
{{formField systemFields.cargo value=system.cargo localize=true}}
{{formField systemFields.tonnage value=system.tonnage localize=true}}
</div>
</fieldset>
</div>
</div>
</fieldset>
</section>

28
templates/weapon.hbs Normal file
View File

@@ -0,0 +1,28 @@
<section>
<div class="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>
<fieldset>
{{formField systemFields.techAge value=system.techAge localize=true}}
{{formField systemFields.weaponType value=system.weaponType localize=true}}
{{formField systemFields.rangeType value=system.rangeType localize=true}}
{{formField systemFields.damage value=system.damage}}
{{formField systemFields.enc value=system.enc}}
{{formField systemFields.cost value=system.cost}}
</fieldset>
<fieldset>
<legend>{{localize "FTLNOMAD.Label.description"}}</legend>
{{formInput
systemFields.description
enriched=description
value=system.description
name="system.description"
toggled=true
}}
</fieldset>
</section>

View File

@@ -0,0 +1,64 @@
import { extractPack, compilePack } from '@foundryvtt/foundryvtt-cli';
import { promises as fs } from 'fs';
import path from "path";
const MODULE_ID = process.cwd();
export class CompendiumsManager {
static async packToDistDir(srcDir = 'packs_src', distDir = 'packs', mode = 'yaml') {
const yaml = mode === 'yaml'
const packs = await fs.readdir('./' + srcDir);
for (const pack of packs) {
if (pack === '.gitattributes') continue;
console.log('Packing ' + pack);
await compilePack(
`${MODULE_ID}/${srcDir}/${pack}`,
`${MODULE_ID}/${distDir}/${pack}`,
{ yaml }
);
}
}
static async unpackToSrcDir(srcDir = 'packs_src', distDir = 'packs', mode = 'yaml') {
const yaml = mode === 'yaml'
const packs = await fs.readdir("./" + distDir);
for (const pack of packs) {
if (pack === ".gitattributes") continue;
if (pack === ".directory") continue;
if (pack.endsWith(".db")) continue;
console.log("Unpacking " + pack);
const directory = `./${srcDir}/${pack}`;
// Create the directory if it doesn't exist
await fs.mkdir(directory, { recursive: true });
try {
for (const file of await fs.readdir(directory)) {
await fs.unlink(path.join(directory, file));
}
} catch (error) {
if (error.code === "ENOENT") console.log("No files inside of " + pack);
else console.log(error);
}
await extractPack(
`${MODULE_ID}/${distDir}/${pack}`,
`${MODULE_ID}/${srcDir}/${pack}`,
{
yaml: mode === 'yaml',
transformName: doc => CompendiumsManager.transformName(doc, mode === 'yaml'),
}
);
}
}
/**
* Prefaces the document with its type
* @param {object} doc - The document data
*/
static transformName(doc, yaml) {
const safeFileName = doc.name.replace(/[^a-zA-Z0-9А-я]/g, "_");
const type = doc._key.split("!")[1];
const prefix = ["actors", "items"].includes(type) ? doc.type : type;
return `${doc.name ? `${prefix}_${safeFileName}_${doc._id}` : doc._id}.${yaml ? "yml" : "json"}`;
}
}

View File

@@ -0,0 +1,3 @@
import { CompendiumsManager } from './CompendiumsManager.mjs';
CompendiumsManager.packToDistDir()

View File

@@ -0,0 +1,3 @@
import { CompendiumsManager } from './CompendiumsManager.mjs';
CompendiumsManager.unpackToSrcDir()