commit b50f08d79b9dfc908ad148d9a234c09bccf4a8f5 Author: Alex Collins Date: Mon May 20 02:59:52 2024 -0700 First commit diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..01017a5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG + +## 1.2.0 + +- Add support for Foundry v10 \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..95b0a54 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,30 @@ +MIT License + +Copyright (c) 2020 Asacolips Projects / Foundry Mods + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +This license does not apply to the compendium content listed in this software's +"packs" directory. See the README for licensing information for the compendium +packs. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +IMAGE LICENSES + +anvil-impact.png by Lorc under CC BY 3.0 from game-icons.net \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1fbc3eb --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Shadowrun6 System + +![Foundry v11](https://img.shields.io/badge/foundry-v11-green) + +This system is a shadowrun-6e-ultimate system that you can use as a starting point for building your own custom systems. It's similar to Simple World-building, but has examples of creating attributes in code rather than dynamically through the UI. + +## Usage + +There are two ways to get started: using the Shadowrun6 system generator command or manually renaming and updating files. + +Regardless of which method you choose, think carefully about your system's name. Your system's package name when submitted to Foundry must be formatted like `alphanumeric-lowercase`, and it must be unique. Check the Foundry systems package list for conflicts before committing to a name! + +> **Data Models** +> +> If you would like to use DataModel classes instead of the older template.json configuration, you'll need to use the `npm run generate` command described below and choose to enable them when asked. DataModels are currently an optional feature, and are only availabe in the generator CLI due to that. + +### Generator + +This system includes a generator CLI in `package.json`. To use it, you must have [node.js](https://nodejs.org) installed, and it's recommended that you install node 20 or later. + +> **Python Generator** +> +> If you would rather use Python than node, there’s an excellent Python-based generator created by Cussa at https://github.com/Cussa/fvtt-shadowrun-6e-ultimate-initializator. Give it a shot! + +Once you have npm installed, you can run the following in your terminal or command prompt: + +```bash +npm install +npm run generate +``` + +Your terminal should prompt you to name your system. Read the instructions carefully, the letter case and special characters in each question matter for correct system generation. + +Once the generator completes, it will output your system to `build/`, where `` is the package name you supplied during the prompt. + +Copy this directory over to your Foundry systems directory and start coding! + +### Manual Replacement + +Before installing this system, you should rename any files that have `shadowrun-6e-ultimate` in their filename to use whatever machine-safe name your system needs, such as `adnd2e` if you were building a system for 2nd edition Advanced Dungeons & Dragons. In addition, you should search through the files for `shadowrun-6e-ultimate` and `Shadowrun6` and do the same for those, replacing them with appropriate names for your system. + +The `name` property in your `system.json` file is your system's package name. This need to be formatted `alphanumeric-lowercase`, and it must also match the foldername you use for your system. + +### Vue 3 Shadowrun6 + +**NOTE: The Vue 3 version is currently outdated and considered an advanced usage of Foundry due to it being a custom renderer. Only try it out if you _really_ like Vue and are feeling dangerous!** + +Alternatively, there's another build of this system that supports using Vue 3 components (ES module build target) for character sheet templates. + +Head over to the [Vue3Shadowrun6 System](https://gitlab.com/asacolips-projects/foundry-mods/vue3shadowrun-6e-ultimate) repo if you're interested in using Vue! + +### Getting Help + +Check out the [Official Foundry VTT Discord](https://discord.gg/foundryvtt)! The #system-development channel has helpful pins and is a good place to ask questions about any part of the foundry application. + +For more static references, the [Knowledge Base](https://foundryvtt.com/kb/) and [API Documentation](https://foundryvtt.com/api/) provide different levels of detail. For the most detail, you can find the client side code in your foundry installation location. Classes are documented in individual files under `resources/app/client` and `resources/app/common`, and the code is collated into a single file at `resources/app/public/scripts/foundry.js`. + +#### Tutorial + +For much more information on how to use this system as a starting point for making your own, see the [full tutorial on the Foundry Wiki](https://foundryvtt.wiki/en/development/guides/SD-tutorial)! + +Note: Tutorial may be out of date, so look out for the Foundry compatibility badge at the top of each page. + +## Sheet Layout + +This system includes a handful of helper CSS classes to help you lay out your sheets if you're not comfortable diving into CSS fully. Those are: + +- `flexcol`: Included by Foundry itself, this lays out the child elements of whatever element you place this on vertically. +- `flexrow`: Included by Foundry itself, this lays out the child elements of whatever element you place this on horizontally. +- `flex-center`: When used on something that's using flexrow or flexcol, this will center the items and text. +- `flex-between`: When used on something that's using flexrow or flexcol, this will attempt to place space between the items. Similar to "justify" in word processors. +- `flex-group-center`: Add a border, padding, and center all items. +- `flex-group-left`: Add a border, padding, and left align all items. +- `flex-group-right`: Add a border, padding, and right align all items. +- `grid`: When combined with the `grid-Ncol` classes, this will lay out child elements in a grid. +- `grid-Ncol`: Replace `N` with any number from 1-12, such as `grid-3col`. When combined with `grid`, this will layout child elements in a grid with a number of columns equal to the number specified. + +## Compiling the CSS + +This repo includes both CSS for the theme and SCSS source files. If you're new to CSS, it's probably easier to just work in those files directly and delete the SCSS directory. If you're interested in using a CSS preprocessor to add support for nesting, variables, and more, you can run `npm install` in this directory to install the dependencies for the scss compiler. After that, just run `npm run build` to compile the SCSS and start a process that watches for new changes. + +![image](http://mattsmith.in/images/shadowrun-6e-ultimate.png) diff --git a/assets/anvil-impact.png b/assets/anvil-impact.png new file mode 100644 index 0000000..b6a6f43 Binary files /dev/null and b/assets/anvil-impact.png differ diff --git a/css/shadowrun-6e-ultimate.css b/css/shadowrun-6e-ultimate.css new file mode 100644 index 0000000..399c9b6 --- /dev/null +++ b/css/shadowrun-6e-ultimate.css @@ -0,0 +1,366 @@ +@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"); +/* Global styles */ +.window-app { + font-family: "Roboto", sans-serif; +} + +.rollable:hover, .rollable:focus { + color: #000; + text-shadow: 0 0 10px red; + cursor: pointer; +} + +.grid, +.grid-2col { + display: grid; + grid-column: span 2/span 2; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin: 10px 0; + padding: 0; +} + +.grid-3col { + grid-column: span 3/span 3; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.grid-4col { + grid-column: span 4/span 4; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.grid-5col { + grid-column: span 5/span 5; + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.grid-6col { + grid-column: span 6/span 6; + grid-template-columns: repeat(6, minmax(0, 1fr)); +} + +.grid-7col { + grid-column: span 7/span 7; + grid-template-columns: repeat(7, minmax(0, 1fr)); +} + +.grid-8col { + grid-column: span 8/span 8; + grid-template-columns: repeat(8, minmax(0, 1fr)); +} + +.grid-9col { + grid-column: span 9/span 9; + grid-template-columns: repeat(9, minmax(0, 1fr)); +} + +.grid-10col { + grid-column: span 10/span 10; + grid-template-columns: repeat(10, minmax(0, 1fr)); +} + +.grid-11col { + grid-column: span 11/span 11; + grid-template-columns: repeat(11, minmax(0, 1fr)); +} + +.grid-12col { + grid-column: span 12/span 12; + grid-template-columns: repeat(12, minmax(0, 1fr)); +} + +.grid-start-2 { + grid-column-start: 2; +} + +.grid-start-3 { + grid-column-start: 3; +} + +.grid-start-4 { + grid-column-start: 4; +} + +.grid-start-5 { + grid-column-start: 5; +} + +.grid-start-6 { + grid-column-start: 6; +} + +.grid-start-7 { + grid-column-start: 7; +} + +.grid-start-8 { + grid-column-start: 8; +} + +.grid-start-9 { + grid-column-start: 9; +} + +.grid-start-10 { + grid-column-start: 10; +} + +.grid-start-11 { + grid-column-start: 11; +} + +.grid-start-12 { + grid-column-start: 12; +} + +.grid-span-2 { + grid-column-end: span 2; +} + +.grid-span-3 { + grid-column-end: span 3; +} + +.grid-span-4 { + grid-column-end: span 4; +} + +.grid-span-5 { + grid-column-end: span 5; +} + +.grid-span-6 { + grid-column-end: span 6; +} + +.grid-span-7 { + grid-column-end: span 7; +} + +.grid-span-8 { + grid-column-end: span 8; +} + +.grid-span-9 { + grid-column-end: span 9; +} + +.grid-span-10 { + grid-column-end: span 10; +} + +.grid-span-11 { + grid-column-end: span 11; +} + +.grid-span-12 { + grid-column-end: span 12; +} + +.flex-group-center, +.flex-group-left, +.flex-group-right { + justify-content: center; + align-items: center; + text-align: center; +} + +.flex-group-left { + justify-content: flex-start; + text-align: left; +} + +.flex-group-right { + justify-content: flex-end; + text-align: right; +} + +.flexshrink { + flex: 0; +} + +.flex-between { + justify-content: space-between; +} + +.flexlarge { + flex: 2; +} + +.align-left { + justify-content: flex-start; + text-align: left; +} + +.align-right { + justify-content: flex-end; + text-align: right; +} + +.align-center { + justify-content: center; + text-align: center; +} + +/* Styles limited to shadowrun-6e-ultimate sheets */ +.shadowrun-6e-ultimate .item-form { + font-family: "Roboto", sans-serif; +} +.shadowrun-6e-ultimate .sheet-header { + flex: 0 auto; + overflow: hidden; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + margin-bottom: 10px; +} +.shadowrun-6e-ultimate .sheet-header .profile-img { + flex: 0 0 100px; + height: 100px; + margin-right: 10px; +} +.shadowrun-6e-ultimate .sheet-header .header-fields { + flex: 1; +} +.shadowrun-6e-ultimate .sheet-header h1.charname { + height: 50px; + padding: 0px; + margin: 5px 0; + border-bottom: 0; +} +.shadowrun-6e-ultimate .sheet-header h1.charname input { + width: 100%; + height: 100%; + margin: 0; +} +.shadowrun-6e-ultimate .sheet-tabs { + flex: 0; +} +.shadowrun-6e-ultimate .sheet-body, +.shadowrun-6e-ultimate .sheet-body .tab, +.shadowrun-6e-ultimate .sheet-body .tab .editor { + height: 100%; +} +.shadowrun-6e-ultimate .tox .tox-editor-container { + background: #fff; +} +.shadowrun-6e-ultimate .tox .tox-edit-area { + padding: 0 8px; +} +.shadowrun-6e-ultimate .resource-label { + font-weight: bold; +} +.shadowrun-6e-ultimate .items-header { + height: 28px; + margin: 2px 0; + padding: 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: 2px groove #eeede0; + font-weight: bold; +} +.shadowrun-6e-ultimate .items-header > * { + font-size: 14px; + text-align: center; +} +.shadowrun-6e-ultimate .items-header .item-name { + font-weight: bold; + padding-left: 5px; + text-align: left; + display: flex; +} +.shadowrun-6e-ultimate .items-list { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; + scrollbar-width: thin; + color: #444; +} +.shadowrun-6e-ultimate .items-list .item-list { + list-style: none; + margin: 0; + padding: 0; +} +.shadowrun-6e-ultimate .items-list .item-name { + flex: 2; + margin: 0; + overflow: hidden; + font-size: 13px; + text-align: left; + align-items: center; + display: flex; +} +.shadowrun-6e-ultimate .items-list .item-name h3, .shadowrun-6e-ultimate .items-list .item-name h4 { + margin: 0; + white-space: nowrap; + overflow-x: hidden; +} +.shadowrun-6e-ultimate .items-list .item-controls { + display: flex; + flex: 0 0 100px; + justify-content: flex-end; +} +.shadowrun-6e-ultimate .items-list .item-controls a { + font-size: 12px; + text-align: center; + margin: 0 6px; +} +.shadowrun-6e-ultimate .items-list .item { + align-items: center; + padding: 0 2px; + border-bottom: 1px solid #c9c7b8; +} +.shadowrun-6e-ultimate .items-list .item:last-child { + border-bottom: none; +} +.shadowrun-6e-ultimate .items-list .item .item-name { + color: #191813; +} +.shadowrun-6e-ultimate .items-list .item .item-name .item-image { + flex: 0 0 30px; + height: 30px; + background-size: 30px; + border: none; + margin-right: 5px; +} +.shadowrun-6e-ultimate .items-list .item-prop { + text-align: center; + border-left: 1px solid #c9c7b8; + border-right: 1px solid #c9c7b8; + font-size: 12px; +} +.shadowrun-6e-ultimate .items-list .items-header { + height: 28px; + margin: 2px 0; + padding: 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: 2px groove #eeede0; + font-weight: bold; +} +.shadowrun-6e-ultimate .items-list .items-header > * { + font-size: 12px; + text-align: center; +} +.shadowrun-6e-ultimate .items-list .items-header .item-name { + padding-left: 5px; + text-align: left; +} +.shadowrun-6e-ultimate .item-formula { + flex: 0 0 200px; + padding: 0 8px; +} +.shadowrun-6e-ultimate .effects .item .effect-source, +.shadowrun-6e-ultimate .effects .item .effect-duration, +.shadowrun-6e-ultimate .effects .item .effect-controls { + text-align: center; + border-left: 1px solid #c9c7b8; + border-right: 1px solid #c9c7b8; + font-size: 12px; +} +.shadowrun-6e-ultimate .effects .item .effect-controls { + border: none; +} diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..e99ee47 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,41 @@ +{ + "SHADOWRUN_6": { + "Ability": { + "Str": { "long": "Strength", "abbr": "str" }, + "Con": { "long": "Constitution", "abbr": "con" }, + "Dex": { "long": "Dexterity", "abbr": "dex" }, + "Int": { "long": "Intelligence", "abbr": "int" }, + "Wis": { "long": "Wisdom", "abbr": "wis" }, + "Cha": { "long": "Charisma", "abbr": "cha" } + }, + "SheetLabels": { + "Actor": "Shadowrun6 Actor Sheet", + "Item": "Shadowrun6 Item Sheet" + }, + "Item": { + "Spell": { + "SpellLVL": "Level {level} Spells", + "AddLVL": "Add LVL {level}" + } + }, + "Effect": { + "Source": "Source", + "Toggle": "Toggle Effect", + "Temporary": "Temporary Effects", + "Passive": "Passive Effects", + "Inactive": "Inactive Effects" + } + }, + + "TYPES": { + "Actor": { + "character": "Character", + "npc": "NPC" + }, + "Item": { + "item": "Item", + "feature": "Feature", + "spell": "Spell" + } + } +} diff --git a/lib/some-lib/some-lib.css b/lib/some-lib/some-lib.css new file mode 100644 index 0000000..e69de29 diff --git a/lib/some-lib/some-lib.min.js b/lib/some-lib/some-lib.min.js new file mode 100644 index 0000000..e69de29 diff --git a/module/data/_module.mjs b/module/data/_module.mjs new file mode 100644 index 0000000..26d4617 --- /dev/null +++ b/module/data/_module.mjs @@ -0,0 +1,10 @@ +// Export Actors +export {default as Shadowrun6ActorBase} from "./actor-base.mjs"; +export {default as Shadowrun6Character} from "./character.mjs"; +export {default as Shadowrun6NPC} from "./npc.mjs"; + +// Export Items +export {default as Shadowrun6ItemBase} from "./item-base.mjs"; +export {default as Shadowrun6Item} from "./item.mjs"; +export {default as Shadowrun6Feature} from "./feature.mjs"; +export {default as Shadowrun6Spell} from "./spell.mjs"; diff --git a/module/data/actor-base.mjs b/module/data/actor-base.mjs new file mode 100644 index 0000000..f41ca0e --- /dev/null +++ b/module/data/actor-base.mjs @@ -0,0 +1,20 @@ +export default class Shadowrun6ActorBase extends foundry.abstract.TypeDataModel { + + static defineSchema() { + const fields = foundry.data.fields; + const requiredInteger = { required: true, nullable: false, integer: true }; + const schema = {}; + + schema.health = new fields.SchemaField({ + value: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }), + max: new fields.NumberField({ ...requiredInteger, initial: 10 }) + }); + schema.power = new fields.SchemaField({ + value: new fields.NumberField({ ...requiredInteger, initial: 5, min: 0 }), + max: new fields.NumberField({ ...requiredInteger, initial: 5 }) + }); + schema.biography = new fields.StringField({ required: true, blank: true }); // equivalent to passing ({initial: ""}) for StringFields + + return schema; + } +} \ No newline at end of file diff --git a/module/data/character.mjs b/module/data/character.mjs new file mode 100644 index 0000000..225bcb7 --- /dev/null +++ b/module/data/character.mjs @@ -0,0 +1,54 @@ +import Shadowrun6ActorBase from "./actor-base.mjs"; + +export default class Shadowrun6Character extends Shadowrun6ActorBase { + + static defineSchema() { + const fields = foundry.data.fields; + const requiredInteger = { required: true, nullable: false, integer: true }; + const schema = super.defineSchema(); + + schema.attributes = new fields.SchemaField({ + level: new fields.SchemaField({ + value: new fields.NumberField({ ...requiredInteger, initial: 1 }) + }), + }); + + // Iterate over ability names and create a new SchemaField for each. + schema.abilities = new fields.SchemaField(Object.keys(CONFIG.SHADOWRUN_6.abilities).reduce((obj, ability) => { + obj[ability] = new fields.SchemaField({ + value: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }), + mod: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), + label: new fields.StringField({ required: true, blank: true }) + }); + return obj; + }, {})); + + return schema; + } + + prepareDerivedData() { + // Loop through ability scores, and add their modifiers to our sheet output. + for (const key in this.abilities) { + // Calculate the modifier using d20 rules. + this.abilities[key].mod = Math.floor((this.abilities[key].value - 10) / 2); + // Handle ability label localization. + this.abilities[key].label = game.i18n.localize(CONFIG.SHADOWRUN_6.abilities[key]) ?? key; + } + } + + getRollData() { + const data = {}; + + // Copy the ability scores to the top level, so that rolls can use + // formulas like `@str.mod + 4`. + if (this.abilities) { + for (let [k,v] of Object.entries(this.abilities)) { + data[k] = foundry.utils.deepClone(v); + } + } + + data.lvl = this.attributes.level.value; + + return data + } +} \ No newline at end of file diff --git a/module/data/feature.mjs b/module/data/feature.mjs new file mode 100644 index 0000000..150c7fd --- /dev/null +++ b/module/data/feature.mjs @@ -0,0 +1,3 @@ +import Shadowrun6ItemBase from "./item-base.mjs"; + +export default class Shadowrun6Feature extends Shadowrun6ItemBase {} \ No newline at end of file diff --git a/module/data/item-base.mjs b/module/data/item-base.mjs new file mode 100644 index 0000000..9f41ec9 --- /dev/null +++ b/module/data/item-base.mjs @@ -0,0 +1,11 @@ +export default class Shadowrun6ItemBase extends foundry.abstract.TypeDataModel { + + static defineSchema() { + const fields = foundry.data.fields; + const schema = {}; + + schema.description = new fields.StringField({ required: true, blank: true }); + + return schema; + } +} \ No newline at end of file diff --git a/module/data/item.mjs b/module/data/item.mjs new file mode 100644 index 0000000..76d67e4 --- /dev/null +++ b/module/data/item.mjs @@ -0,0 +1,31 @@ +import Shadowrun6ItemBase from "./item-base.mjs"; + +export default class Shadowrun6Item extends Shadowrun6ItemBase { + + static defineSchema() { + const fields = foundry.data.fields; + const requiredInteger = { required: true, nullable: false, integer: true }; + const schema = super.defineSchema(); + + schema.quantity = new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 }); + schema.weight = new fields.NumberField({ required: true, nullable: false, initial: 0, min: 0 }); + + // Break down roll formula into three independent fields + schema.roll = new fields.SchemaField({ + diceNum: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 }), + diceSize: new fields.StringField({ initial: "d20" }), + diceBonus: new fields.StringField({ initial: "+@str.mod+ceil(@lvl / 2)" }) + }) + + schema.formula = new fields.StringField({ blank: true }); + + return schema; + } + + prepareDerivedData() { + // Build the formula dynamically using string interpolation + const roll = this.roll; + + this.formula = `${roll.diceNum}${roll.diceSize}${roll.diceBonus}` + } +} \ No newline at end of file diff --git a/module/data/npc.mjs b/module/data/npc.mjs new file mode 100644 index 0000000..eecdd15 --- /dev/null +++ b/module/data/npc.mjs @@ -0,0 +1,19 @@ +import Shadowrun6ActorBase from "./actor-base.mjs"; + +export default class Shadowrun6NPC extends Shadowrun6ActorBase { + + static defineSchema() { + const fields = foundry.data.fields; + const requiredInteger = { required: true, nullable: false, integer: true }; + const schema = super.defineSchema(); + + schema.cr = new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }); + schema.xp = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }); + + return schema + } + + prepareDerivedData() { + this.xp = this.cr * this.cr * 100; + } +} \ No newline at end of file diff --git a/module/data/spell.mjs b/module/data/spell.mjs new file mode 100644 index 0000000..ca5b891 --- /dev/null +++ b/module/data/spell.mjs @@ -0,0 +1,13 @@ +import Shadowrun6ItemBase from "./item-base.mjs"; + +export default class Shadowrun6Spell extends Shadowrun6ItemBase { + + static defineSchema() { + const fields = foundry.data.fields; + const schema = super.defineSchema(); + + schema.spellLevel = new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 1, max: 9 }); + + return schema; + } +} \ No newline at end of file diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs new file mode 100644 index 0000000..b0de5d0 --- /dev/null +++ b/module/documents/actor.mjs @@ -0,0 +1,45 @@ +/** + * Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system. + * @extends {Actor} + */ +export class Shadowrun6Actor extends Actor { + /** @override */ + prepareData() { + // Prepare data for the actor. Calling the super version of this executes + // the following, in order: data reset (to clear active effects), + // prepareBaseData(), prepareEmbeddedDocuments() (including active effects), + // prepareDerivedData(). + super.prepareData(); + } + + /** @override */ + prepareBaseData() { + // Data modifications in this step occur before processing embedded + // documents or derived data. + } + + /** + * @override + * Augment the actor source data with additional dynamic data that isn't + * handled by the actor's DataModel. Data calculated in this step should be + * available both inside and outside of character sheets (such as if an actor + * is queried and has a roll executed directly from it). + */ + prepareDerivedData() { + const actorData = this; + const flags = actorData.flags.shadowrun6eultimate || {}; + } + + /** + * + * @override + * Augment the actor's default getRollData() method by appending the data object + * generated by the its DataModel's getRollData(), or null. This polymorphic + * approach is useful when you have actors & items that share a parent Document, + * but have slightly different data preparation needs. + */ + getRollData() { + return { ...super.getRollData(), ...this.system.getRollData?.() ?? null }; + } + +} diff --git a/module/documents/item.mjs b/module/documents/item.mjs new file mode 100644 index 0000000..d43a200 --- /dev/null +++ b/module/documents/item.mjs @@ -0,0 +1,71 @@ +/** + * Extend the basic Item with some very simple modifications. + * @extends {Item} + */ +export class Shadowrun6Item extends Item { + /** + * Augment the basic Item data model with additional dynamic data. + */ + prepareData() { + // As with the actor class, items are documents that can have their data + // preparation methods overridden (such as prepareBaseData()). + super.prepareData(); + } + + /** + * Prepare a data object which defines the data schema used by dice roll commands against this Item + * @override + */ + getRollData() { + // Starts off by populating the roll data with `this.system` + const rollData = { ...super.getRollData() }; + + // Quit early if there's no parent actor + if (!this.actor) return rollData; + + // If present, add the actor's roll data + rollData.actor = this.actor.getRollData(); + + return rollData; + } + + /** + * Handle clickable rolls. + * @param {Event} event The originating click event + * @private + */ + async roll() { + const item = this; + + // Initialize chat data. + const speaker = ChatMessage.getSpeaker({ actor: this.actor }); + const rollMode = game.settings.get('core', 'rollMode'); + const label = `[${item.type}] ${item.name}`; + + // If there's no roll data, send a chat message. + if (!this.system.formula) { + ChatMessage.create({ + speaker: speaker, + rollMode: rollMode, + flavor: label, + content: item.system.description ?? '', + }); + } + // Otherwise, create a roll and send a chat message from it. + else { + // Retrieve roll data. + const rollData = this.getRollData(); + + // Invoke the roll and submit it to chat. + const roll = new Roll(rollData.formula, rollData.actor); + // If you need to store the value first, uncomment the next line. + // const result = await roll.evaluate(); + roll.toMessage({ + speaker: speaker, + rollMode: rollMode, + flavor: label, + }); + return roll; + } + } +} diff --git a/module/helpers/config.mjs b/module/helpers/config.mjs new file mode 100644 index 0000000..a64d7eb --- /dev/null +++ b/module/helpers/config.mjs @@ -0,0 +1,23 @@ +export const SHADOWRUN_6 = {}; + +/** + * The set of Ability Scores used within the system. + * @type {Object} + */ +SHADOWRUN_6.abilities = { + str: 'SHADOWRUN_6.Ability.Str.long', + dex: 'SHADOWRUN_6.Ability.Dex.long', + con: 'SHADOWRUN_6.Ability.Con.long', + int: 'SHADOWRUN_6.Ability.Int.long', + wis: 'SHADOWRUN_6.Ability.Wis.long', + cha: 'SHADOWRUN_6.Ability.Cha.long', +}; + +SHADOWRUN_6.abilityAbbreviations = { + str: 'SHADOWRUN_6.Ability.Str.abbr', + dex: 'SHADOWRUN_6.Ability.Dex.abbr', + con: 'SHADOWRUN_6.Ability.Con.abbr', + int: 'SHADOWRUN_6.Ability.Int.abbr', + wis: 'SHADOWRUN_6.Ability.Wis.abbr', + cha: 'SHADOWRUN_6.Ability.Cha.abbr', +}; diff --git a/module/helpers/effects.mjs b/module/helpers/effects.mjs new file mode 100644 index 0000000..6238ba8 --- /dev/null +++ b/module/helpers/effects.mjs @@ -0,0 +1,68 @@ +/** + * Manage Active Effect instances through an Actor or Item Sheet via effect control buttons. + * @param {MouseEvent} event The left-click event on the effect control + * @param {Actor|Item} owner The owning document which manages this effect + */ +export function onManageActiveEffect(event, owner) { + event.preventDefault(); + const a = event.currentTarget; + const li = a.closest('li'); + const effect = li.dataset.effectId + ? owner.effects.get(li.dataset.effectId) + : null; + switch (a.dataset.action) { + case 'create': + return owner.createEmbeddedDocuments('ActiveEffect', [ + { + name: game.i18n.format('DOCUMENT.New', { + type: game.i18n.localize('DOCUMENT.ActiveEffect'), + }), + icon: 'icons/svg/aura.svg', + origin: owner.uuid, + 'duration.rounds': + li.dataset.effectType === 'temporary' ? 1 : undefined, + disabled: li.dataset.effectType === 'inactive', + }, + ]); + case 'edit': + return effect.sheet.render(true); + case 'delete': + return effect.delete(); + case 'toggle': + return effect.update({ disabled: !effect.disabled }); + } +} + +/** + * Prepare the data structure for Active Effects which are currently embedded in an Actor or Item. + * @param {ActiveEffect[]} effects A collection or generator of Active Effect documents to prepare sheet data for + * @return {object} Data for rendering + */ +export function prepareActiveEffectCategories(effects) { + // Define effect header categories + const categories = { + temporary: { + type: 'temporary', + label: game.i18n.localize('SHADOWRUN_6.Effect.Temporary'), + effects: [], + }, + passive: { + type: 'passive', + label: game.i18n.localize('SHADOWRUN_6.Effect.Passive'), + effects: [], + }, + inactive: { + type: 'inactive', + label: game.i18n.localize('SHADOWRUN_6.Effect.Inactive'), + effects: [], + }, + }; + + // Iterate over active effects, classifying them into categories + for (let e of effects) { + if (e.disabled) categories.inactive.effects.push(e); + else if (e.isTemporary) categories.temporary.effects.push(e); + else categories.passive.effects.push(e); + } + return categories; +} diff --git a/module/helpers/templates.mjs b/module/helpers/templates.mjs new file mode 100644 index 0000000..503ee30 --- /dev/null +++ b/module/helpers/templates.mjs @@ -0,0 +1,16 @@ +/** + * Define a set of template paths to pre-load + * Pre-loaded templates are compiled and cached for fast access when rendering + * @return {Promise} + */ +export const preloadHandlebarsTemplates = async function () { + return loadTemplates([ + // Actor partials. + 'systems/shadowrun-6e-ultimate/templates/actor/parts/actor-features.hbs', + 'systems/shadowrun-6e-ultimate/templates/actor/parts/actor-items.hbs', + 'systems/shadowrun-6e-ultimate/templates/actor/parts/actor-spells.hbs', + 'systems/shadowrun-6e-ultimate/templates/actor/parts/actor-effects.hbs', + // Item partials + 'systems/shadowrun-6e-ultimate/templates/item/parts/item-effects.hbs', + ]); +}; diff --git a/module/shadowrun-6e-ultimate.mjs b/module/shadowrun-6e-ultimate.mjs new file mode 100644 index 0000000..fd48eef --- /dev/null +++ b/module/shadowrun-6e-ultimate.mjs @@ -0,0 +1,158 @@ +// Import document classes. +import { Shadowrun6Actor } from './documents/actor.mjs'; +import { Shadowrun6Item } from './documents/item.mjs'; +// Import sheet classes. +import { Shadowrun6ActorSheet } from './sheets/actor-sheet.mjs'; +import { Shadowrun6ItemSheet } from './sheets/item-sheet.mjs'; +// Import helper/utility classes and constants. +import { preloadHandlebarsTemplates } from './helpers/templates.mjs'; +import { SHADOWRUN_6 } from './helpers/config.mjs'; +// Import DataModel classes +import * as models from './data/_module.mjs'; + +/* -------------------------------------------- */ +/* Init Hook */ +/* -------------------------------------------- */ + +Hooks.once('init', function () { + // Add utility classes to the global game object so that they're more easily + // accessible in global contexts. + game.shadowrun6eultimate = { + Shadowrun6Actor, + Shadowrun6Item, + rollItemMacro, + }; + + // Add custom constants for configuration. + CONFIG.SHADOWRUN_6 = SHADOWRUN_6; + + /** + * Set an initiative formula for the system + * @type {String} + */ + CONFIG.Combat.initiative = { + formula: '1d20 + @abilities.dex.mod', + decimals: 2, + }; + + // Define custom Document and DataModel classes + CONFIG.Actor.documentClass = Shadowrun6Actor; + + // Note that you don't need to declare a DataModel + // for the base actor/item classes - they are included + // with the Character/NPC as part of super.defineSchema() + CONFIG.Actor.dataModels = { + character: models.Shadowrun6Character, + npc: models.Shadowrun6NPC + } + CONFIG.Item.documentClass = Shadowrun6Item; + CONFIG.Item.dataModels = { + item: models.Shadowrun6Item, + feature: models.Shadowrun6Feature, + spell: models.Shadowrun6Spell + } + + // Active Effects are never copied to the Actor, + // but will still apply to the Actor from within the Item + // if the transfer property on the Active Effect is true. + CONFIG.ActiveEffect.legacyTransferral = false; + + // Register sheet application classes + Actors.unregisterSheet('core', ActorSheet); + Actors.registerSheet('shadowrun-6e-ultimate', Shadowrun6ActorSheet, { + makeDefault: true, + label: 'SHADOWRUN_6.SheetLabels.Actor', + }); + Items.unregisterSheet('core', ItemSheet); + Items.registerSheet('shadowrun-6e-ultimate', Shadowrun6ItemSheet, { + makeDefault: true, + label: 'SHADOWRUN_6.SheetLabels.Item', + }); + + // Preload Handlebars templates. + return preloadHandlebarsTemplates(); +}); + +/* -------------------------------------------- */ +/* Handlebars Helpers */ +/* -------------------------------------------- */ + +// If you need to add Handlebars helpers, here is a useful example: +Handlebars.registerHelper('toLowerCase', function (str) { + return str.toLowerCase(); +}); + +/* -------------------------------------------- */ +/* Ready Hook */ +/* -------------------------------------------- */ + +Hooks.once('ready', function () { + // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to + Hooks.on('hotbarDrop', (bar, data, slot) => createItemMacro(data, slot)); +}); + +/* -------------------------------------------- */ +/* Hotbar Macros */ +/* -------------------------------------------- */ + +/** + * Create a Macro from an Item drop. + * Get an existing item macro if one exists, otherwise create a new one. + * @param {Object} data The dropped data + * @param {number} slot The hotbar slot to use + * @returns {Promise} + */ +async function createItemMacro(data, slot) { + // First, determine if this is a valid owned item. + if (data.type !== 'Item') return; + if (!data.uuid.includes('Actor.') && !data.uuid.includes('Token.')) { + return ui.notifications.warn( + 'You can only create macro buttons for owned Items' + ); + } + // If it is, retrieve it based on the uuid. + const item = await Item.fromDropData(data); + + // Create the macro command using the uuid. + const command = `game.shadowrun6eultimate.rollItemMacro("${data.uuid}");`; + let macro = game.macros.find( + (m) => m.name === item.name && m.command === command + ); + if (!macro) { + macro = await Macro.create({ + name: item.name, + type: 'script', + img: item.img, + command: command, + flags: { 'shadowrun-6e-ultimate.itemMacro': true }, + }); + } + game.user.assignHotbarMacro(macro, slot); + return false; +} + +/** + * Create a Macro from an Item drop. + * Get an existing item macro if one exists, otherwise create a new one. + * @param {string} itemUuid + */ +function rollItemMacro(itemUuid) { + // Reconstruct the drop data so that we can load the item. + const dropData = { + type: 'Item', + uuid: itemUuid, + }; + // Load the item from the uuid. + Item.fromDropData(dropData).then((item) => { + // Determine if the item loaded and if it's an owned item. + if (!item || !item.parent) { + const itemName = item?.name ?? itemUuid; + return ui.notifications.warn( + `Could not find item ${itemName}. You may need to delete and recreate this macro.` + ); + } + + // Trigger the item roll + item.roll(); + }); +} diff --git a/module/sheets/actor-sheet.mjs b/module/sheets/actor-sheet.mjs new file mode 100644 index 0000000..7dda059 --- /dev/null +++ b/module/sheets/actor-sheet.mjs @@ -0,0 +1,246 @@ +import { + onManageActiveEffect, + prepareActiveEffectCategories, +} from '../helpers/effects.mjs'; + +/** + * Extend the basic ActorSheet with some very simple modifications + * @extends {ActorSheet} + */ +export class Shadowrun6ActorSheet extends ActorSheet { + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ['shadowrun-6e-ultimate', 'sheet', 'actor'], + width: 600, + height: 600, + tabs: [ + { + navSelector: '.sheet-tabs', + contentSelector: '.sheet-body', + initial: 'features', + }, + ], + }); + } + + /** @override */ + get template() { + return `systems/shadowrun-6e-ultimate/templates/actor/actor-${this.actor.type}-sheet.hbs`; + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + // Retrieve the data structure from the base sheet. You can inspect or log + // the context variable to see the structure, but some key properties for + // sheets are the actor object, the data object, whether or not it's + // editable, the items array, and the effects array. + const context = super.getData(); + + // Use a safe clone of the actor data for further operations. + const actorData = context.data; + + // Add the actor's data to context.data for easier access, as well as flags. + context.system = actorData.system; + context.flags = actorData.flags; + + // Prepare character data and items. + if (actorData.type == 'character') { + this._prepareItems(context); + this._prepareCharacterData(context); + } + + // Prepare NPC data and items. + if (actorData.type == 'npc') { + this._prepareItems(context); + } + + // Add roll data for TinyMCE editors. + context.rollData = context.actor.getRollData(); + + // Prepare active effects + context.effects = prepareActiveEffectCategories( + // A generator that returns all effects stored on the actor + // as well as any items + this.actor.allApplicableEffects() + ); + + return context; + } + + /** + * Organize and classify Items for Character sheets. + * + * @param {Object} actorData The actor to prepare. + * + * @return {undefined} + */ + _prepareCharacterData(context) { + // Handle ability scores. + // for (let [k, v] of Object.entries(context.system.abilities)) { + // v.label = game.i18n.localize(CONFIG.SHADOWRUN_6.abilities[k]) ?? k; + // } + } + + /** + * Organize and classify Items for Character sheets. + * + * @param {Object} actorData The actor to prepare. + * + * @return {undefined} + */ + _prepareItems(context) { + // Initialize containers. + const gear = []; + const features = []; + const spells = { + 0: [], + 1: [], + 2: [], + 3: [], + 4: [], + 5: [], + 6: [], + 7: [], + 8: [], + 9: [], + }; + + // Iterate through items, allocating to containers + for (let i of context.items) { + i.img = i.img || Item.DEFAULT_ICON; + // Append to gear. + if (i.type === 'item') { + gear.push(i); + } + // Append to features. + else if (i.type === 'feature') { + features.push(i); + } + // Append to spells. + else if (i.type === 'spell') { + if (i.system.spellLevel != undefined) { + spells[i.system.spellLevel].push(i); + } + } + } + + // Assign and return + context.gear = gear; + context.features = features; + context.spells = spells; + } + + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + + // Render the item sheet for viewing/editing prior to the editable check. + html.on('click', '.item-edit', (ev) => { + const li = $(ev.currentTarget).parents('.item'); + const item = this.actor.items.get(li.data('itemId')); + item.sheet.render(true); + }); + + // ------------------------------------------------------------- + // Everything below here is only needed if the sheet is editable + if (!this.isEditable) return; + + // Add Inventory Item + html.on('click', '.item-create', this._onItemCreate.bind(this)); + + // Delete Inventory Item + html.on('click', '.item-delete', (ev) => { + const li = $(ev.currentTarget).parents('.item'); + const item = this.actor.items.get(li.data('itemId')); + item.delete(); + li.slideUp(200, () => this.render(false)); + }); + + // Active Effect management + html.on('click', '.effect-control', (ev) => { + const row = ev.currentTarget.closest('li'); + const document = + row.dataset.parentId === this.actor.id + ? this.actor + : this.actor.items.get(row.dataset.parentId); + onManageActiveEffect(ev, document); + }); + + // Rollable abilities. + html.on('click', '.rollable', this._onRoll.bind(this)); + + // Drag events for macros. + if (this.actor.isOwner) { + let handler = (ev) => this._onDragStart(ev); + html.find('li.item').each((i, li) => { + if (li.classList.contains('inventory-header')) return; + li.setAttribute('draggable', true); + li.addEventListener('dragstart', handler, false); + }); + } + } + + /** + * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset + * @param {Event} event The originating click event + * @private + */ + async _onItemCreate(event) { + event.preventDefault(); + const header = event.currentTarget; + // Get the type of item to create. + const type = header.dataset.type; + // Grab any data associated with this control. + const data = duplicate(header.dataset); + // Initialize a default name. + const name = `New ${type.capitalize()}`; + // Prepare the item object. + const itemData = { + name: name, + type: type, + system: data, + }; + // Remove the type from the dataset since it's in the itemData.type prop. + delete itemData.system['type']; + + // Finally, create the item! + return await Item.create(itemData, { parent: this.actor }); + } + + /** + * Handle clickable rolls. + * @param {Event} event The originating click event + * @private + */ + _onRoll(event) { + event.preventDefault(); + const element = event.currentTarget; + const dataset = element.dataset; + + // Handle item rolls. + if (dataset.rollType) { + if (dataset.rollType == 'item') { + const itemId = element.closest('.item').dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) return item.roll(); + } + } + + // Handle rolls that supply the formula directly. + if (dataset.roll) { + let label = dataset.label ? `[ability] ${dataset.label}` : ''; + let roll = new Roll(dataset.roll, this.actor.getRollData()); + roll.toMessage({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + flavor: label, + rollMode: game.settings.get('core', 'rollMode'), + }); + return roll; + } + } +} diff --git a/module/sheets/item-sheet.mjs b/module/sheets/item-sheet.mjs new file mode 100644 index 0000000..d9ddc74 --- /dev/null +++ b/module/sheets/item-sheet.mjs @@ -0,0 +1,77 @@ +import { + onManageActiveEffect, + prepareActiveEffectCategories, +} from '../helpers/effects.mjs'; + +/** + * Extend the basic ItemSheet with some very simple modifications + * @extends {ItemSheet} + */ +export class Shadowrun6ItemSheet extends ItemSheet { + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ['shadowrun-6e-ultimate', 'sheet', 'item'], + width: 520, + height: 480, + tabs: [ + { + navSelector: '.sheet-tabs', + contentSelector: '.sheet-body', + initial: 'description', + }, + ], + }); + } + + /** @override */ + get template() { + const path = 'systems/shadowrun-6e-ultimate/templates/item'; + // Return a single sheet for all item types. + // return `${path}/item-sheet.hbs`; + + // Alternatively, you could use the following return statement to do a + // unique item sheet by type, like `weapon-sheet.hbs`. + return `${path}/item-${this.item.type}-sheet.hbs`; + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + // Retrieve base data structure. + const context = super.getData(); + + // Use a safe clone of the item data for further operations. + const itemData = context.data; + + // Retrieve the roll data for TinyMCE editors. + context.rollData = this.item.getRollData(); + + // Add the item's data to context.data for easier access, as well as flags. + context.system = itemData.system; + context.flags = itemData.flags; + + // Prepare active effects for easier access + context.effects = prepareActiveEffectCategories(this.item.effects); + + return context; + } + + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + + // Everything below here is only needed if the sheet is editable + if (!this.isEditable) return; + + // Roll handlers, click handlers, etc. would go here. + + // Active Effect management + html.on('click', '.effect-control', (ev) => + onManageActiveEffect(ev, this.item) + ); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e9264a --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "shadowrun-6e-ultimate", + "version": "2.0.0", + "description": "CSS compiler for the Shadowrun6 system", + "scripts": { + "build": "sass src/scss/shadowrun-6e-ultimate.scss css/shadowrun-6e-ultimate.css --style=expanded --no-source-map", + "watch": "sass src/scss/shadowrun-6e-ultimate.scss css/shadowrun-6e-ultimate.css --style=expanded --source-map --watch" + }, + "browserslist": [ + "last 3 versions" + ], + "author": "Asacolips", + "license": "MIT", + "private": true, + "devDependencies": { + "sass": "^1.53.0" + } +} \ No newline at end of file diff --git a/packs/.gitattributes b/packs/.gitattributes new file mode 100644 index 0000000..23d6d82 --- /dev/null +++ b/packs/.gitattributes @@ -0,0 +1 @@ +/** binary \ No newline at end of file diff --git a/src/scss/components/_effects.scss b/src/scss/components/_effects.scss new file mode 100644 index 0000000..4c3b2c7 --- /dev/null +++ b/src/scss/components/_effects.scss @@ -0,0 +1,14 @@ +.effects .item { + .effect-source, + .effect-duration, + .effect-controls { + text-align: center; + border-left: 1px solid #c9c7b8; + border-right: 1px solid #c9c7b8; + font-size: 12px; + } + + .effect-controls { + border: none; + } +} diff --git a/src/scss/components/_forms.scss b/src/scss/components/_forms.scss new file mode 100644 index 0000000..0e21369 --- /dev/null +++ b/src/scss/components/_forms.scss @@ -0,0 +1,55 @@ +.item-form { + font-family: $font-primary; +} + +.sheet-header { + flex: 0 auto; + overflow: hidden; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + margin-bottom: 10px; + + .profile-img { + flex: 0 0 100px; + height: 100px; + margin-right: 10px; + } + + .header-fields { + flex: 1; + } + + h1.charname { + height: 50px; + padding: 0px; + margin: 5px 0; + border-bottom: 0; + input { + width: 100%; + height: 100%; + margin: 0; + } + } +} + +.sheet-tabs { + flex: 0; +} + +.sheet-body, +.sheet-body .tab, +.sheet-body .tab .editor { + height: 100%; +} + +.tox { + .tox-editor-container { + background: $c-white; + } + + .tox-edit-area { + padding: 0 8px; + } +} \ No newline at end of file diff --git a/src/scss/components/_items.scss b/src/scss/components/_items.scss new file mode 100644 index 0000000..ea3a50c --- /dev/null +++ b/src/scss/components/_items.scss @@ -0,0 +1,121 @@ +// Section Header +.items-header { + height: 28px; + margin: 2px 0; + padding: 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: $border-groove; + font-weight: bold; + > * { + font-size: 14px; + text-align: center; + } + .item-name { + font-weight: bold; + padding-left: 5px; + text-align: left; + display: flex; + // font-size: 16px; + } +} + +// ----------------------------------------- +// Items Lists +// ----------------------------------------- + +.items-list { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; + scrollbar-width: thin; + color: $c-tan; + + // Child lists + .item-list { + list-style: none; + margin: 0; + padding: 0; + } + + // Item Name + .item-name { + flex: 2; + margin: 0; + overflow: hidden; + font-size: 13px; + text-align: left; + align-items: center; + display: flex; + h3, h4 { + margin: 0; + white-space: nowrap; + overflow-x: hidden; + } + } + + // Control Buttons + .item-controls { + display: flex; + flex: 0 0 100px; + justify-content: flex-end; + a { + font-size: 12px; + text-align: center; + margin: 0 6px; + } + } + + // Individual Item + .item { + align-items: center; + padding: 0 2px; // to align with the header border + border-bottom: 1px solid $c-faint; + &:last-child { border-bottom: none; } + .item-name { + color: $c-dark; + .item-image { + flex: 0 0 30px; + height: 30px; + background-size: 30px; + border: none; + margin-right: 5px; + } + } + } + + .item-prop { + text-align: center; + border-left: 1px solid #c9c7b8; + border-right: 1px solid #c9c7b8; + font-size: 12px; + } + + // Section Header + .items-header { + height: 28px; + margin: 2px 0; + padding: 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: $border-groove; + font-weight: bold; + > * { + font-size: 12px; + text-align: center; + } + .item-name { + padding-left: 5px; + text-align: left; + // font-size: 16px; + } + } +} + +// Example style for Shadowrun6 (can be removed if not needed). +.item-formula { + + flex: 0 0 200px; + padding: 0 8px; +} \ No newline at end of file diff --git a/src/scss/components/_resource.scss b/src/scss/components/_resource.scss new file mode 100644 index 0000000..5c9b910 --- /dev/null +++ b/src/scss/components/_resource.scss @@ -0,0 +1,3 @@ +.resource-label { + font-weight: bold; +} \ No newline at end of file diff --git a/src/scss/global/_flex.scss b/src/scss/global/_flex.scss new file mode 100644 index 0000000..a5a5666 --- /dev/null +++ b/src/scss/global/_flex.scss @@ -0,0 +1,46 @@ +// Flexbox. +.flex-group-center, +.flex-group-left, +.flex-group-right { + justify-content: center; + align-items: center; + text-align: center; +} + +.flex-group-left { + justify-content: flex-start; + text-align: left; +} + +.flex-group-right { + justify-content: flex-end; + text-align: right; +} + +.flexshrink { + flex: 0; +} + +.flex-between { + justify-content: space-between; +} + +.flexlarge { + flex: 2; +} + +// Alignment styles. +.align-left { + justify-content: flex-start; + text-align: left; +} + +.align-right { + justify-content: flex-end; + text-align: right; +} + +.align-center { + justify-content: center; + text-align: center; +} \ No newline at end of file diff --git a/src/scss/global/_grid.scss b/src/scss/global/_grid.scss new file mode 100644 index 0000000..16839df --- /dev/null +++ b/src/scss/global/_grid.scss @@ -0,0 +1,85 @@ +// Grid. +.grid, +.grid-2col { + display: grid; + grid-column: span 2 / span 2; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin: 10px 0; + padding: 0; +} + +.grid-3col { + grid-column: span 3 / span 3; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.grid-4col { + grid-column: span 4 / span 4; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.grid-5col { + grid-column: span 5 / span 5; + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.grid-6col { + grid-column: span 6 / span 6; + grid-template-columns: repeat(6, minmax(0, 1fr)); +} + +.grid-7col { + grid-column: span 7 / span 7; + grid-template-columns: repeat(7, minmax(0, 1fr)); +} + +.grid-8col { + grid-column: span 8 / span 8; + grid-template-columns: repeat(8, minmax(0, 1fr)); +} + +.grid-9col { + grid-column: span 9 / span 9; + grid-template-columns: repeat(9, minmax(0, 1fr)); +} + +.grid-10col { + grid-column: span 10 / span 10; + grid-template-columns: repeat(10, minmax(0, 1fr)); +} + +.grid-11col { + grid-column: span 11 / span 11; + grid-template-columns: repeat(11, minmax(0, 1fr)); +} + +.grid-12col { + grid-column: span 12 / span 12; + grid-template-columns: repeat(12, minmax(0, 1fr)); +} + +// Grid offset. +.grid-start-2 { grid-column-start: 2 } +.grid-start-3 { grid-column-start: 3 } +.grid-start-4 { grid-column-start: 4 } +.grid-start-5 { grid-column-start: 5 } +.grid-start-6 { grid-column-start: 6 } +.grid-start-7 { grid-column-start: 7 } +.grid-start-8 { grid-column-start: 8 } +.grid-start-9 { grid-column-start: 9 } +.grid-start-10 { grid-column-start: 10 } +.grid-start-11 { grid-column-start: 11 } +.grid-start-12 { grid-column-start: 12 } + +.grid-span-2 { grid-column-end: span 2 } +.grid-span-3 { grid-column-end: span 3 } +.grid-span-4 { grid-column-end: span 4 } +.grid-span-5 { grid-column-end: span 5 } +.grid-span-6 { grid-column-end: span 6 } +.grid-span-7 { grid-column-end: span 7 } +.grid-span-8 { grid-column-end: span 8 } +.grid-span-9 { grid-column-end: span 9 } +.grid-span-10 { grid-column-end: span 10 } +.grid-span-11 { grid-column-end: span 11 } +.grid-span-12 { grid-column-end: span 12 } diff --git a/src/scss/global/_window.scss b/src/scss/global/_window.scss new file mode 100644 index 0000000..97581e5 --- /dev/null +++ b/src/scss/global/_window.scss @@ -0,0 +1,12 @@ +.window-app { + font-family: $font-primary; +} + +.rollable { + &:hover, + &:focus { + color: #000; + text-shadow: 0 0 10px red; + cursor: pointer; + } +} \ No newline at end of file diff --git a/src/scss/shadowrun-6e-ultimate.scss b/src/scss/shadowrun-6e-ultimate.scss new file mode 100644 index 0000000..939c447 --- /dev/null +++ b/src/scss/shadowrun-6e-ultimate.scss @@ -0,0 +1,21 @@ +// Add custom fonts by visiting and search https://fonts.google.com +@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); + +// Import utilities. +@import 'utils/typography'; +@import 'utils/colors'; +@import 'utils/mixins'; +@import 'utils/variables'; + +/* Global styles */ +@import 'global/window'; +@import 'global/grid'; +@import 'global/flex'; + +/* Styles limited to shadowrun-6e-ultimate sheets */ +.shadowrun-6e-ultimate { + @import 'components/forms'; + @import 'components/resource'; + @import 'components/items'; + @import 'components/effects'; +} diff --git a/src/scss/utils/_colors.scss b/src/scss/utils/_colors.scss new file mode 100644 index 0000000..fc43cab --- /dev/null +++ b/src/scss/utils/_colors.scss @@ -0,0 +1,7 @@ +$c-white: #fff; +$c-black: #000; + +$c-dark: #191813; +$c-faint: #c9c7b8; +$c-beige: #b5b3a4; +$c-tan: #444; \ No newline at end of file diff --git a/src/scss/utils/_mixins.scss b/src/scss/utils/_mixins.scss new file mode 100644 index 0000000..69dd2ad --- /dev/null +++ b/src/scss/utils/_mixins.scss @@ -0,0 +1,16 @@ +@mixin element-invisible { + position: absolute; + + width: 1px; + height: 1px; + margin: -1px; + border: 0; + padding: 0; + + clip: rect(0 0 0 0); + overflow: hidden; +} + +@mixin hide { + display: none; +} \ No newline at end of file diff --git a/src/scss/utils/_typography.scss b/src/scss/utils/_typography.scss new file mode 100644 index 0000000..0c44234 --- /dev/null +++ b/src/scss/utils/_typography.scss @@ -0,0 +1,2 @@ +$font-primary: 'Roboto', sans-serif; +$font-secondary: 'Roboto', sans-serif; \ No newline at end of file diff --git a/src/scss/utils/_variables.scss b/src/scss/utils/_variables.scss new file mode 100644 index 0000000..6d22f61 --- /dev/null +++ b/src/scss/utils/_variables.scss @@ -0,0 +1,5 @@ +$padding-sm: 5px; +$padding-md: 10px; +$padding-lg: 20px; + +$border-groove: 2px groove #eeede0; \ No newline at end of file diff --git a/system.json b/system.json new file mode 100644 index 0000000..13afe84 --- /dev/null +++ b/system.json @@ -0,0 +1,51 @@ +{ + "id": "shadowrun-6e-ultimate", + "title": "Shadowrun 6 Ultimania", + "description": "The Shadowrun 6 Ultimania system for FoundryVTT!", + "authors": [ + { + "name": "Asacolips", + "discord": "asacolips" + }, + { + "name": "Lee Talman", + "discord": ".pizzaWizard" + } + ], + "url": "Replace this with a link to your public repository", + "license": "LICENSE.txt", + "readme": "README.md", + "bugs": "Replace this with a link to file issues or tickets", + "changelog": "CHANGELOG.md", + "media": [ + { + "type": "setup", + "url": "systems/shadowrun-6e-ultimate/assets/anvil-impact.png", + "thumbnail": "systems/shadowrun-6e-ultimate/assets/anvil-impact.png" + } + ], + "version": "2.0.0", + "compatibility": { + "minimum": 11, + "verified": "11.315" + }, + "esmodules": ["module/shadowrun-6e-ultimate.mjs"], + "styles": ["css/shadowrun-6e-ultimate.css"], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "packs": [], + "packFolders": [], + "socket": false, + "manifest": "Replace this with a link to the /latest release system.json so people can receive updates after they've installed the system", + "download": "This must link to a .zip file of the current version", + "background": "systems/shadowrun-6e-ultimate/assets/anvil-impact.png", + "gridDistance": 5, + "gridUnits": "ft", + "primaryTokenAttribute": "health", + "secondaryTokenAttribute": "power" +} diff --git a/template.json b/template.json new file mode 100644 index 0000000..434bb9f --- /dev/null +++ b/template.json @@ -0,0 +1,8 @@ +{ + "Actor": { + "types": ["character", "npc"] + }, + "Item": { + "types": ["item", "feature", "spell"] + } +} diff --git a/templates/actor/actor-character-sheet.hbs b/templates/actor/actor-character-sheet.hbs new file mode 100644 index 0000000..090370a --- /dev/null +++ b/templates/actor/actor-character-sheet.hbs @@ -0,0 +1,111 @@ +
+ + {{!-- Sheet Header --}} +
+ +
+

+ {{!-- The grid classes are defined in scss/global/_grid.scss. To use, + use both the "grid" and "grid-Ncol" class where "N" can be any number + from 1 to 12 and will create that number of columns. --}} +
+ + {{!-- "flex-group-center" is also defined in the _grid.scss file + and it will add a small amount of padding, a border, and will + center all of its child elements content and text. --}} +
+ +
+ + / + +
+
+ +
+ +
+ + / + +
+
+ +
+ +
+ +
+
+ +
+
+
+ + {{!-- Sheet Tab Navigation --}} + + + {{!-- Sheet Body --}} +
+ + {{!-- Owned Features Tab --}} +
+
+ + + {{!-- For the main features list, span the right two columns --}} +
+ {{!-- This is a Handlebars partial. They're stored in the `/parts` folder next to this sheet, and defined in module/helpers/templates.mjs --}} + {{> "systems/shadowrun-6e-ultimate/templates/actor/parts/actor-features.hbs"}} +
+ +
+
+ + {{!-- Biography Tab --}} +
+ {{!-- If you want TinyMCE editors to output inline rolls when rendered, you need to pass the actor's roll data to the rollData property. --}} + {{editor system.biography target="system.biography" rollData=rollData button=true owner=owner editable=editable}} +
+ + {{!-- Owned Items Tab --}} +
+ {{> "systems/shadowrun-6e-ultimate/templates/actor/parts/actor-items.hbs"}} +
+ + {{!-- Owned Spells Tab --}} +
+ {{> "systems/shadowrun-6e-ultimate/templates/actor/parts/actor-spells.hbs"}} +
+ + {{!-- Active Effects Tab --}} +
+ {{> "systems/shadowrun-6e-ultimate/templates/actor/parts/actor-effects.hbs"}} +
+ +
+
+ +{{log system}} + diff --git a/templates/actor/actor-npc-sheet.hbs b/templates/actor/actor-npc-sheet.hbs new file mode 100644 index 0000000..33e683d --- /dev/null +++ b/templates/actor/actor-npc-sheet.hbs @@ -0,0 +1,76 @@ +
+ + {{!-- Sheet Header --}} +
+ +
+

+ {{!-- The grid classes are defined in scss/global/_grid.scss. To use, + use both the "grid" and "grid-Ncol" class where "N" can be any number + from 1 to 12 and will create that number of columns. --}} +
+ + {{!-- "flex-group-center" is also defined in the _grid.scss file + and it will add a small amount of padding, a border, and will + center all of its child elements content and text. --}} +
+ +
+ + / + +
+
+ +
+ +
+ + / + +
+
+ +
+ +
+ + / + +
+
+ +
+
+
+ + {{!-- Sheet Tab Navigation --}} + + + {{!-- Sheet Body --}} +
+ + {{!-- Biography Tab --}} +
+ {{!-- If you want TinyMCE editors to output inline rolls when rendered, you need to pass the actor's roll data to the rollData property. --}} + {{editor system.biography target="system.biography" rollData=rollData button=true owner=owner editable=editable}} +
+ + {{!-- Owned Items Tab --}} +
+ {{> "systems/shadowrun-6e-ultimate/templates/actor/parts/actor-items.hbs"}} +
+ + {{!-- Active Effects Tab --}} +
+ {{> "systems/shadowrun-6e-ultimate/templates/actor/parts/actor-effects.hbs"}} +
+ +
+
+ diff --git a/templates/actor/parts/actor-effects.hbs b/templates/actor/parts/actor-effects.hbs new file mode 100644 index 0000000..95c6e33 --- /dev/null +++ b/templates/actor/parts/actor-effects.hbs @@ -0,0 +1,38 @@ +
    + {{#each effects as |section sid|}} +
  1. +

    {{localize section.label}}

    +
    {{localize "SHADOWRUN_6.Effect.Source"}}
    +
    {{localize "EFFECT.TabDuration"}}
    + +
  2. + +
      + {{#each section.effects as |effect|}} +
    1. +
      + +

      {{effect.name}}

      +
      +
      {{effect.sourceName}}
      +
      {{effect.duration.label}}
      + +
    2. + {{/each}} +
    + {{/each}} +
diff --git a/templates/actor/parts/actor-features.hbs b/templates/actor/parts/actor-features.hbs new file mode 100644 index 0000000..889de0e --- /dev/null +++ b/templates/actor/parts/actor-features.hbs @@ -0,0 +1,46 @@ +
    +
  1. +
    {{localize 'Name'}}
    + +
  2. + {{#each features as |item id|}} +
  3. +
    +
    + + + +
    +

    {{item.name}}

    +
    + +
  4. + {{/each}} +
\ No newline at end of file diff --git a/templates/actor/parts/actor-items.hbs b/templates/actor/parts/actor-items.hbs new file mode 100644 index 0000000..3a84cad --- /dev/null +++ b/templates/actor/parts/actor-items.hbs @@ -0,0 +1,48 @@ +
    +
  1. +
    {{localize 'Name'}}
    +
    {{localize 'Roll Formula'}}
    + +
  2. + {{#each gear as |item id|}} +
  3. +
    +
    + + + +
    +

    {{item.name}}

    +
    +
    {{item.system.formula}}
    + +
  4. + {{/each}} +
\ No newline at end of file diff --git a/templates/actor/parts/actor-spells.hbs b/templates/actor/parts/actor-spells.hbs new file mode 100644 index 0000000..d79f0a1 --- /dev/null +++ b/templates/actor/parts/actor-spells.hbs @@ -0,0 +1,49 @@ +
    + {{#each spells as |spells spellLevel|}} +
  1. +
    + {{localize 'SHADOWRUN_6.Item.Spell.SpellLVL' level=spellLevel}} +
    + +
  2. + {{#each spells as |item id|}} +
  3. +
    +
    + +
    +

    {{item.name}}

    +
    + +
  4. + {{/each}} + {{/each}} +
\ No newline at end of file diff --git a/templates/item/item-feature-sheet.hbs b/templates/item/item-feature-sheet.hbs new file mode 100644 index 0000000..e2c4fdc --- /dev/null +++ b/templates/item/item-feature-sheet.hbs @@ -0,0 +1,35 @@ +
+
+ +
+

+
+
+ + {{!-- Sheet Tab Navigation --}} + + + {{!-- Sheet Body --}} +
+ + {{!-- Description Tab --}} +
+ {{editor system.description target="system.description" rollData=rollData button=true owner=owner editable=editable}} +
+ + {{!-- Attributes Tab --}} +
+ {{!-- As you add new fields, add them in here! --}} +
+ + {{!-- Effects Tab --}} +
+ {{> "systems/shadowrun-6e-ultimate/templates/item/parts/item-effects.hbs"}} +
+
+ +
diff --git a/templates/item/item-item-sheet.hbs b/templates/item/item-item-sheet.hbs new file mode 100644 index 0000000..2a9aeff --- /dev/null +++ b/templates/item/item-item-sheet.hbs @@ -0,0 +1,62 @@ +
+
+ +
+

+
+
+ + +
+
+ + +
+
+
+
+ + {{!-- Sheet Tab Navigation --}} + + + {{!-- Sheet Body --}} +
+ + {{!-- Description Tab --}} +
+ {{!-- To render inline rolls in a TinyMCE editor, you need to pass the parent actor's (if any) roll data to the + rollData prop. --}} + {{editor system.description target="system.description" rollData=rollData button=true owner=owner + editable=editable}} +
+ + {{!-- Attributes Tab --}} +
+ {{!-- As you add new fields, add them in here! --}} +
+ + {{system.formula}} +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +{{log system}} \ No newline at end of file diff --git a/templates/item/item-sheet.hbs b/templates/item/item-sheet.hbs new file mode 100644 index 0000000..d247dbc --- /dev/null +++ b/templates/item/item-sheet.hbs @@ -0,0 +1,30 @@ +{{!-- This template is a fallback for when items don't have more specific templates. --}} +{{!-- Generally, you'll want to make more specific templates when possible. --}} +
+
+ +
+

+
+
+ + {{!-- Sheet Tab Navigation --}} + + + {{!-- Sheet Body --}} +
+ + {{!-- Description Tab --}} +
+ {{editor system.description target="system.description" rollData=rollData button=true owner=owner editable=editable}} +
+ + {{!-- Attributes Tab --}} +
+ {{!-- As you add new fields, add them in here! --}} +
+
+
diff --git a/templates/item/item-spell-sheet.hbs b/templates/item/item-spell-sheet.hbs new file mode 100644 index 0000000..7ebafdc --- /dev/null +++ b/templates/item/item-spell-sheet.hbs @@ -0,0 +1,32 @@ +
+
+ +
+

+
+
+ + {{!-- Sheet Tab Navigation --}} + + + {{!-- Sheet Body --}} +
+ + {{!-- Description Tab --}} +
+ {{editor system.description target="system.description" rollData=rollData button=true owner=owner editable=editable}} +
+ + {{!-- Attributes Tab --}} +
+ {{!-- As you add new fields, add them in here! --}} +
+ + +
+
+
+
diff --git a/templates/item/parts/item-effects.hbs b/templates/item/parts/item-effects.hbs new file mode 100644 index 0000000..7c4f1fd --- /dev/null +++ b/templates/item/parts/item-effects.hbs @@ -0,0 +1,38 @@ +
    + {{#each effects as |section sid|}} +
  1. +

    {{localize section.label}}

    +
    {{localize "SHADOWRUN_6.Effect.Source"}}
    +
    {{localize "EFFECT.TabDuration"}}
    + +
  2. + +
      + {{#each section.effects as |effect|}} +
    1. +
      + +

      {{effect.name}}

      +
      +
      {{effect.sourceName}}
      +
      {{effect.duration.label}}
      + +
    2. + {{/each}} +
    + {{/each}} +