commit c0223977d2e9a52bf0d7473075fdf56762f5b970 Author: LeRatierBretonnier Date: Tue May 5 13:55:42 2026 +0200 System development, WIP diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57a4b68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.github/ +rules/ +node_modules/ diff --git a/.history/system_20260504170857.json b/.history/system_20260504170857.json new file mode 100644 index 0000000..6484277 --- /dev/null +++ b/.history/system_20260504170857.json @@ -0,0 +1,94 @@ +{ + "id": "fvtt-machine-gods-noxian-expanse", + "title": "Machine Gods of the Noxian Expanse", + "description": "Foundry VTT system for Machine Gods of the Noxian Expanse.", + "version": "14..0", + "authors": [ + { + "name": "Uberwald" + } + ], + "compatibility": { + "minimum": "13", + "verified": "14" + }, + "esmodules": [ + "fvtt-machine-gods-noxian-expanse.mjs" + ], + "styles": [ + "css/mgne.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "documentTypes": { + "Actor": { + "character": { + "htmlFields": [ + "description", + "notes" + ] + }, + "creature": { + "htmlFields": [ + "description", + "special", + "notes" + ] + }, + "companion": { + "htmlFields": [ + "description", + "notes" + ] + } + }, + "Item": { + "weapon": { + "htmlFields": [ + "description" + ] + }, + "armor": { + "htmlFields": [ + "description" + ] + }, + "shield": { + "htmlFields": [ + "description" + ] + }, + "equipment": { + "htmlFields": [ + "description" + ] + }, + "resonance-core": { + "htmlFields": [ + "description" + ] + }, + "artifact": { + "htmlFields": [ + "description" + ] + }, + "feature": { + "htmlFields": [ + "description" + ] + } + } + }, + "grid": { + "distance": 10, + "units": "ft" + }, + "primaryTokenAttribute": "hp", + "socket": false +} diff --git a/.history/system_20260504170858.json b/.history/system_20260504170858.json new file mode 100644 index 0000000..6484277 --- /dev/null +++ b/.history/system_20260504170858.json @@ -0,0 +1,94 @@ +{ + "id": "fvtt-machine-gods-noxian-expanse", + "title": "Machine Gods of the Noxian Expanse", + "description": "Foundry VTT system for Machine Gods of the Noxian Expanse.", + "version": "14..0", + "authors": [ + { + "name": "Uberwald" + } + ], + "compatibility": { + "minimum": "13", + "verified": "14" + }, + "esmodules": [ + "fvtt-machine-gods-noxian-expanse.mjs" + ], + "styles": [ + "css/mgne.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "documentTypes": { + "Actor": { + "character": { + "htmlFields": [ + "description", + "notes" + ] + }, + "creature": { + "htmlFields": [ + "description", + "special", + "notes" + ] + }, + "companion": { + "htmlFields": [ + "description", + "notes" + ] + } + }, + "Item": { + "weapon": { + "htmlFields": [ + "description" + ] + }, + "armor": { + "htmlFields": [ + "description" + ] + }, + "shield": { + "htmlFields": [ + "description" + ] + }, + "equipment": { + "htmlFields": [ + "description" + ] + }, + "resonance-core": { + "htmlFields": [ + "description" + ] + }, + "artifact": { + "htmlFields": [ + "description" + ] + }, + "feature": { + "htmlFields": [ + "description" + ] + } + } + }, + "grid": { + "distance": 10, + "units": "ft" + }, + "primaryTokenAttribute": "hp", + "socket": false +} diff --git a/.history/system_20260504170859.json b/.history/system_20260504170859.json new file mode 100644 index 0000000..27c18c3 --- /dev/null +++ b/.history/system_20260504170859.json @@ -0,0 +1,94 @@ +{ + "id": "fvtt-machine-gods-noxian-expanse", + "title": "Machine Gods of the Noxian Expanse", + "description": "Foundry VTT system for Machine Gods of the Noxian Expanse.", + "version": "14.0.0", + "authors": [ + { + "name": "Uberwald" + } + ], + "compatibility": { + "minimum": "13", + "verified": "14" + }, + "esmodules": [ + "fvtt-machine-gods-noxian-expanse.mjs" + ], + "styles": [ + "css/mgne.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "documentTypes": { + "Actor": { + "character": { + "htmlFields": [ + "description", + "notes" + ] + }, + "creature": { + "htmlFields": [ + "description", + "special", + "notes" + ] + }, + "companion": { + "htmlFields": [ + "description", + "notes" + ] + } + }, + "Item": { + "weapon": { + "htmlFields": [ + "description" + ] + }, + "armor": { + "htmlFields": [ + "description" + ] + }, + "shield": { + "htmlFields": [ + "description" + ] + }, + "equipment": { + "htmlFields": [ + "description" + ] + }, + "resonance-core": { + "htmlFields": [ + "description" + ] + }, + "artifact": { + "htmlFields": [ + "description" + ] + }, + "feature": { + "htmlFields": [ + "description" + ] + } + } + }, + "grid": { + "distance": 10, + "units": "ft" + }, + "primaryTokenAttribute": "hp", + "socket": false +} diff --git a/.history/system_20260504170903.json b/.history/system_20260504170903.json new file mode 100644 index 0000000..d1a12af --- /dev/null +++ b/.history/system_20260504170903.json @@ -0,0 +1,94 @@ +{ + "id": "fvtt-machine-gods-noxian-expanse", + "title": "Machine Gods of the Noxian Expanse", + "description": "Foundry VTT system for Machine Gods of the Noxian Expanse.", + "version": "14.0.0", + "authors": [ + { + "name": "Uberwald" + } + ], + "compatibility": { + "minimum": "14", + "verified": "14" + }, + "esmodules": [ + "fvtt-machine-gods-noxian-expanse.mjs" + ], + "styles": [ + "css/mgne.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "documentTypes": { + "Actor": { + "character": { + "htmlFields": [ + "description", + "notes" + ] + }, + "creature": { + "htmlFields": [ + "description", + "special", + "notes" + ] + }, + "companion": { + "htmlFields": [ + "description", + "notes" + ] + } + }, + "Item": { + "weapon": { + "htmlFields": [ + "description" + ] + }, + "armor": { + "htmlFields": [ + "description" + ] + }, + "shield": { + "htmlFields": [ + "description" + ] + }, + "equipment": { + "htmlFields": [ + "description" + ] + }, + "resonance-core": { + "htmlFields": [ + "description" + ] + }, + "artifact": { + "htmlFields": [ + "description" + ] + }, + "feature": { + "htmlFields": [ + "description" + ] + } + } + }, + "grid": { + "distance": 10, + "units": "ft" + }, + "primaryTokenAttribute": "hp", + "socket": false +} diff --git a/.history/system_20260504170905.json b/.history/system_20260504170905.json new file mode 100644 index 0000000..d1a12af --- /dev/null +++ b/.history/system_20260504170905.json @@ -0,0 +1,94 @@ +{ + "id": "fvtt-machine-gods-noxian-expanse", + "title": "Machine Gods of the Noxian Expanse", + "description": "Foundry VTT system for Machine Gods of the Noxian Expanse.", + "version": "14.0.0", + "authors": [ + { + "name": "Uberwald" + } + ], + "compatibility": { + "minimum": "14", + "verified": "14" + }, + "esmodules": [ + "fvtt-machine-gods-noxian-expanse.mjs" + ], + "styles": [ + "css/mgne.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "documentTypes": { + "Actor": { + "character": { + "htmlFields": [ + "description", + "notes" + ] + }, + "creature": { + "htmlFields": [ + "description", + "special", + "notes" + ] + }, + "companion": { + "htmlFields": [ + "description", + "notes" + ] + } + }, + "Item": { + "weapon": { + "htmlFields": [ + "description" + ] + }, + "armor": { + "htmlFields": [ + "description" + ] + }, + "shield": { + "htmlFields": [ + "description" + ] + }, + "equipment": { + "htmlFields": [ + "description" + ] + }, + "resonance-core": { + "htmlFields": [ + "description" + ] + }, + "artifact": { + "htmlFields": [ + "description" + ] + }, + "feature": { + "htmlFields": [ + "description" + ] + } + } + }, + "grid": { + "distance": 10, + "units": "ft" + }, + "primaryTokenAttribute": "hp", + "socket": false +} diff --git a/assets/fonts/CASTOR TWO W01 REGULAR.TTF b/assets/fonts/CASTOR TWO W01 REGULAR.TTF new file mode 100644 index 0000000..006accf Binary files /dev/null and b/assets/fonts/CASTOR TWO W01 REGULAR.TTF differ diff --git a/assets/fonts/COALITION_V2..TTF b/assets/fonts/COALITION_V2..TTF new file mode 100644 index 0000000..3252f4d Binary files /dev/null and b/assets/fonts/COALITION_V2..TTF differ diff --git a/assets/fonts/LORA-BOLDITALIC.TTF b/assets/fonts/LORA-BOLDITALIC.TTF new file mode 100644 index 0000000..12dea8c Binary files /dev/null and b/assets/fonts/LORA-BOLDITALIC.TTF differ diff --git a/assets/fonts/LORA-REGULAR.TTF b/assets/fonts/LORA-REGULAR.TTF new file mode 100644 index 0000000..dc751db Binary files /dev/null and b/assets/fonts/LORA-REGULAR.TTF differ diff --git a/assets/fonts/alegreya/Alegreya-Italic-VF.ttf b/assets/fonts/alegreya/Alegreya-Italic-VF.ttf new file mode 100644 index 0000000..890f8f1 Binary files /dev/null and b/assets/fonts/alegreya/Alegreya-Italic-VF.ttf differ diff --git a/assets/fonts/alegreya/Alegreya-VF.ttf b/assets/fonts/alegreya/Alegreya-VF.ttf new file mode 100644 index 0000000..af2b220 Binary files /dev/null and b/assets/fonts/alegreya/Alegreya-VF.ttf differ diff --git a/assets/fonts/alegreya/OFL.txt b/assets/fonts/alegreya/OFL.txt new file mode 100644 index 0000000..1a25b6d --- /dev/null +++ b/assets/fonts/alegreya/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Alegreya Project Authors (https://github.com/huertatipografica/Alegreya) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/cinzel-decorative/CinzelDecorative-Bold.ttf b/assets/fonts/cinzel-decorative/CinzelDecorative-Bold.ttf new file mode 100644 index 0000000..7bb0f35 Binary files /dev/null and b/assets/fonts/cinzel-decorative/CinzelDecorative-Bold.ttf differ diff --git a/assets/fonts/cinzel-decorative/CinzelDecorative-Regular.ttf b/assets/fonts/cinzel-decorative/CinzelDecorative-Regular.ttf new file mode 100644 index 0000000..2308d34 Binary files /dev/null and b/assets/fonts/cinzel-decorative/CinzelDecorative-Regular.ttf differ diff --git a/assets/fonts/cinzel-decorative/OFL.txt b/assets/fonts/cinzel-decorative/OFL.txt new file mode 100644 index 0000000..efe1168 --- /dev/null +++ b/assets/fonts/cinzel-decorative/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2012 Natanael Gama (info@ndiscovered.com), with Reserved Font Name 'Cinzel' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/icons/accelerate-core.svg b/assets/icons/accelerate-core.svg new file mode 100644 index 0000000..b4fce8c --- /dev/null +++ b/assets/icons/accelerate-core.svg @@ -0,0 +1,26 @@ + + Accelerate Core + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/akimbo-hit-priest.svg b/assets/icons/akimbo-hit-priest.svg new file mode 100644 index 0000000..54b18da --- /dev/null +++ b/assets/icons/akimbo-hit-priest.svg @@ -0,0 +1,26 @@ + + Akimbo Hit Priest + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ambivalent-slouch.svg b/assets/icons/ambivalent-slouch.svg new file mode 100644 index 0000000..d69d4e5 --- /dev/null +++ b/assets/icons/ambivalent-slouch.svg @@ -0,0 +1,26 @@ + + Ambivalent Slouch + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor.svg b/assets/icons/armor.svg new file mode 100644 index 0000000..ec55e01 --- /dev/null +++ b/assets/icons/armor.svg @@ -0,0 +1,26 @@ + + armor + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/artifact.svg b/assets/icons/artifact.svg new file mode 100644 index 0000000..e5bee9f --- /dev/null +++ b/assets/icons/artifact.svg @@ -0,0 +1,26 @@ + + artifact + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/avaricious-gubbingrifter.svg b/assets/icons/avaricious-gubbingrifter.svg new file mode 100644 index 0000000..4d117f9 --- /dev/null +++ b/assets/icons/avaricious-gubbingrifter.svg @@ -0,0 +1,26 @@ + + Avaricious Gubbingrifter + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/backbiter.svg b/assets/icons/backbiter.svg new file mode 100644 index 0000000..e407823 --- /dev/null +++ b/assets/icons/backbiter.svg @@ -0,0 +1,26 @@ + + Backbiter + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/backpack.svg b/assets/icons/backpack.svg new file mode 100644 index 0000000..69521c8 --- /dev/null +++ b/assets/icons/backpack.svg @@ -0,0 +1,26 @@ + + Backpack + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/blast-core.svg b/assets/icons/blast-core.svg new file mode 100644 index 0000000..dfeea81 --- /dev/null +++ b/assets/icons/blast-core.svg @@ -0,0 +1,26 @@ + + Blast Core + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/cauterize-core.svg b/assets/icons/cauterize-core.svg new file mode 100644 index 0000000..f97b702 --- /dev/null +++ b/assets/icons/cauterize-core.svg @@ -0,0 +1,26 @@ + + Cauterize Core + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/chain-shirt.svg b/assets/icons/chain-shirt.svg new file mode 100644 index 0000000..6637bd6 --- /dev/null +++ b/assets/icons/chain-shirt.svg @@ -0,0 +1,26 @@ + + Chain Shirt + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/chart-reading-maniac.svg b/assets/icons/chart-reading-maniac.svg new file mode 100644 index 0000000..0ef5b61 --- /dev/null +++ b/assets/icons/chart-reading-maniac.svg @@ -0,0 +1,26 @@ + + Chart Reading Maniac + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/clothing-average.svg b/assets/icons/clothing-average.svg new file mode 100644 index 0000000..3765d23 --- /dev/null +++ b/assets/icons/clothing-average.svg @@ -0,0 +1,26 @@ + + Clothing (Average) + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/club.svg b/assets/icons/club.svg new file mode 100644 index 0000000..68b5f2d --- /dev/null +++ b/assets/icons/club.svg @@ -0,0 +1,26 @@ + + Club + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/dagger.svg b/assets/icons/dagger.svg new file mode 100644 index 0000000..e2f9390 --- /dev/null +++ b/assets/icons/dagger.svg @@ -0,0 +1,26 @@ + + Dagger + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/empower-weapon-core.svg b/assets/icons/empower-weapon-core.svg new file mode 100644 index 0000000..0d1ee5b --- /dev/null +++ b/assets/icons/empower-weapon-core.svg @@ -0,0 +1,26 @@ + + Empower Weapon Core + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/equipment.svg b/assets/icons/equipment.svg new file mode 100644 index 0000000..508e4f8 --- /dev/null +++ b/assets/icons/equipment.svg @@ -0,0 +1,26 @@ + + equipment + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/feature.svg b/assets/icons/feature.svg new file mode 100644 index 0000000..943d6f5 --- /dev/null +++ b/assets/icons/feature.svg @@ -0,0 +1,26 @@ + + feature + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/full-plate.svg b/assets/icons/full-plate.svg new file mode 100644 index 0000000..6afa7e8 --- /dev/null +++ b/assets/icons/full-plate.svg @@ -0,0 +1,26 @@ + + Full Plate + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/gambeson.svg b/assets/icons/gambeson.svg new file mode 100644 index 0000000..3e9b486 --- /dev/null +++ b/assets/icons/gambeson.svg @@ -0,0 +1,26 @@ + + Gambeson + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/halberd.svg b/assets/icons/halberd.svg new file mode 100644 index 0000000..f148d43 --- /dev/null +++ b/assets/icons/halberd.svg @@ -0,0 +1,26 @@ + + Halberd + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/half-plate.svg b/assets/icons/half-plate.svg new file mode 100644 index 0000000..face5d6 --- /dev/null +++ b/assets/icons/half-plate.svg @@ -0,0 +1,26 @@ + + Half Plate + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/handaxe.svg b/assets/icons/handaxe.svg new file mode 100644 index 0000000..2cd9bb6 --- /dev/null +++ b/assets/icons/handaxe.svg @@ -0,0 +1,26 @@ + + Handaxe + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/heavy-crossbow.svg b/assets/icons/heavy-crossbow.svg new file mode 100644 index 0000000..ce5cbef --- /dev/null +++ b/assets/icons/heavy-crossbow.svg @@ -0,0 +1,26 @@ + + Heavy Crossbow + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/helm.svg b/assets/icons/helm.svg new file mode 100644 index 0000000..e794eaa --- /dev/null +++ b/assets/icons/helm.svg @@ -0,0 +1,26 @@ + + Helm + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/liturgicantal-blesswell.svg b/assets/icons/liturgicantal-blesswell.svg new file mode 100644 index 0000000..2484bfa --- /dev/null +++ b/assets/icons/liturgicantal-blesswell.svg @@ -0,0 +1,26 @@ + + Liturgicantal Blesswell + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/longsword.svg b/assets/icons/longsword.svg new file mode 100644 index 0000000..034cae0 --- /dev/null +++ b/assets/icons/longsword.svg @@ -0,0 +1,26 @@ + + Longsword + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/lux-relay-idol.svg b/assets/icons/lux-relay-idol.svg new file mode 100644 index 0000000..4223ade --- /dev/null +++ b/assets/icons/lux-relay-idol.svg @@ -0,0 +1,26 @@ + + Lux Relay Idol + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/maul.svg b/assets/icons/maul.svg new file mode 100644 index 0000000..f86c257 --- /dev/null +++ b/assets/icons/maul.svg @@ -0,0 +1,26 @@ + + Maul + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/medical-supplies.svg b/assets/icons/medical-supplies.svg new file mode 100644 index 0000000..0cee493 --- /dev/null +++ b/assets/icons/medical-supplies.svg @@ -0,0 +1,26 @@ + + Medical Supplies + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/medium-shield.svg b/assets/icons/medium-shield.svg new file mode 100644 index 0000000..e7bd2db --- /dev/null +++ b/assets/icons/medium-shield.svg @@ -0,0 +1,26 @@ + + Medium Shield + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/null-forge-spike.svg b/assets/icons/null-forge-spike.svg new file mode 100644 index 0000000..26cdc95 --- /dev/null +++ b/assets/icons/null-forge-spike.svg @@ -0,0 +1,26 @@ + + Null Forge Spike + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/padded-leather.svg b/assets/icons/padded-leather.svg new file mode 100644 index 0000000..02f8758 --- /dev/null +++ b/assets/icons/padded-leather.svg @@ -0,0 +1,26 @@ + + Padded Leather + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/processional-halo.svg b/assets/icons/processional-halo.svg new file mode 100644 index 0000000..ea24eb2 --- /dev/null +++ b/assets/icons/processional-halo.svg @@ -0,0 +1,26 @@ + + Processional Halo + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/quarterstaff.svg b/assets/icons/quarterstaff.svg new file mode 100644 index 0000000..9301e10 --- /dev/null +++ b/assets/icons/quarterstaff.svg @@ -0,0 +1,26 @@ + + Quarterstaff + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/rapier.svg b/assets/icons/rapier.svg new file mode 100644 index 0000000..d4f5d80 --- /dev/null +++ b/assets/icons/rapier.svg @@ -0,0 +1,26 @@ + + Rapier + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/rations.svg b/assets/icons/rations.svg new file mode 100644 index 0000000..25127a5 --- /dev/null +++ b/assets/icons/rations.svg @@ -0,0 +1,26 @@ + + Rations + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/resonance-core.svg b/assets/icons/resonance-core.svg new file mode 100644 index 0000000..5b28cfe --- /dev/null +++ b/assets/icons/resonance-core.svg @@ -0,0 +1,26 @@ + + resonance core + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/rope.svg b/assets/icons/rope.svg new file mode 100644 index 0000000..7b72058 --- /dev/null +++ b/assets/icons/rope.svg @@ -0,0 +1,26 @@ + + Rope + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/sack.svg b/assets/icons/sack.svg new file mode 100644 index 0000000..262269d --- /dev/null +++ b/assets/icons/sack.svg @@ -0,0 +1,26 @@ + + Sack + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/shield.svg b/assets/icons/shield.svg new file mode 100644 index 0000000..1e9772c --- /dev/null +++ b/assets/icons/shield.svg @@ -0,0 +1,26 @@ + + shield + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/shiver-lens.svg b/assets/icons/shiver-lens.svg new file mode 100644 index 0000000..61250c7 --- /dev/null +++ b/assets/icons/shiver-lens.svg @@ -0,0 +1,26 @@ + + Shiver Lens + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/shortbow.svg b/assets/icons/shortbow.svg new file mode 100644 index 0000000..d0de2ec --- /dev/null +++ b/assets/icons/shortbow.svg @@ -0,0 +1,26 @@ + + Shortbow + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/spear.svg b/assets/icons/spear.svg new file mode 100644 index 0000000..6453131 --- /dev/null +++ b/assets/icons/spear.svg @@ -0,0 +1,26 @@ + + Spear + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/waterskin.svg b/assets/icons/waterskin.svg new file mode 100644 index 0000000..e14ccba --- /dev/null +++ b/assets/icons/waterskin.svg @@ -0,0 +1,26 @@ + + Waterskin + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapon.svg b/assets/icons/weapon.svg new file mode 100644 index 0000000..2c39b41 --- /dev/null +++ b/assets/icons/weapon.svg @@ -0,0 +1,26 @@ + + weapon + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/whip.svg b/assets/icons/whip.svg new file mode 100644 index 0000000..e045fc5 --- /dev/null +++ b/assets/icons/whip.svg @@ -0,0 +1,26 @@ + + Whip + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/ui/blackoath_logo.webp b/assets/ui/blackoath_logo.webp new file mode 100644 index 0000000..0a3b90f Binary files /dev/null and b/assets/ui/blackoath_logo.webp differ diff --git a/assets/ui/main_logo.webp b/assets/ui/main_logo.webp new file mode 100644 index 0000000..1bbb138 Binary files /dev/null and b/assets/ui/main_logo.webp differ diff --git a/assets/ui/page_background.webp b/assets/ui/page_background.webp new file mode 100644 index 0000000..c5f0f25 Binary files /dev/null and b/assets/ui/page_background.webp differ diff --git a/assets/ui/welcome_page.webp b/assets/ui/welcome_page.webp new file mode 100644 index 0000000..76860d4 Binary files /dev/null and b/assets/ui/welcome_page.webp differ diff --git a/css/mgne.css b/css/mgne.css new file mode 100644 index 0000000..71ddd58 --- /dev/null +++ b/css/mgne.css @@ -0,0 +1,1021 @@ +@font-face { + font-family: "CastorTwoMGNE"; + src: url("../assets/fonts/CASTOR TWO W01 REGULAR.TTF") format("truetype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "LoraMGNE"; + src: url("../assets/fonts/LORA-REGULAR.TTF") format("truetype"); + font-weight: 400; + font-style: normal; +} +.application.mgne, +.mgne-chat-card { + --mgne-bg: #1b1512; + --mgne-panel: rgba(41, 30, 24, 0.88); + --mgne-panel-soft: rgba(64, 47, 37, 0.66); + --mgne-border: rgba(171, 139, 104, 0.35); + --mgne-border-strong: rgba(196, 154, 69, 0.42); + --mgne-accent: #dd6b2d; + --mgne-accent-alt: #4f7d73; + --mgne-text: #ccb292; + --mgne-text-soft: #7e664f; + --mgne-title: #ab8b68; + --mgne-shadow: 0 14px 30px rgba(0, 0, 0, 0.36); +} +.application.mgne { + font-family: "LoraMGNE", "Book Antiqua", serif; + line-height: 1.45; + color: #ccb292; +} +.application.mgne .window-header { + background: linear-gradient(90deg, rgba(127, 29, 23, 0.4), rgba(183, 70, 31, 0.18) 25%, transparent 70%), linear-gradient(180deg, color-mix(in srgb, #1b1512 80%, black), #13100f); + border-bottom: 1px solid rgba(196, 154, 69, 0.35); + color: #ab8b68; +} +.application.mgne .window-header .window-title { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 1rem; + text-shadow: 0 0 16px rgba(221, 107, 45, 0.25); +} +.application.mgne .window-header .header-control { + font-family: var(--font-awesome, "Font Awesome 6 Pro"); + font-style: normal; + font-variant: normal; + text-rendering: auto; +} +.application.mgne .window-header .header-control::before, +.application.mgne .window-header i[class^="fa"], +.application.mgne .window-header i[class*=" fa"] { + font-family: inherit; + font-style: normal; + font-variant: normal; + text-rendering: auto; +} +.application.mgne .window-header .header-control.fa-solid, +.application.mgne .window-header .header-control.fas, +.application.mgne .window-header .fa-solid, +.application.mgne .window-header .fas { + font-weight: 900; +} +.application.mgne .window-header .header-control.fa-regular, +.application.mgne .window-header .header-control.far, +.application.mgne .window-header .fa-regular, +.application.mgne .window-header .far { + font-weight: 400; +} +.application.mgne .window-content { + background: radial-gradient(circle at top left, rgba(183, 70, 31, 0.12), transparent 28%), radial-gradient(circle at top right, rgba(79, 125, 115, 0.1), transparent 24%), linear-gradient(180deg, rgba(27, 21, 18, 0.3), rgba(19, 16, 15, 0.28)), url("../assets/ui/page_background.webp") center top / cover no-repeat, linear-gradient(180deg, #1b1512, #13100f 120%); + color: #ccb292; + min-height: 0; + overflow-y: auto; + scrollbar-gutter: stable; +} +.application.mgne.character .window-content { + display: flex; + flex-direction: column; + overflow: hidden; +} +.application.mgne *, +.mgne-chat-card * { + box-sizing: border-box; +} +.application.mgne input, +.application.mgne select, +.application.mgne textarea, +.application.mgne button, +.application.mgne label, +.application.mgne p, +.application.mgne span, +.application.mgne small, +.mgne-chat-card { + font-family: "LoraMGNE", "Book Antiqua", serif; + line-height: 1.45; +} +.application.mgne input, +.application.mgne select, +.application.mgne textarea { + width: 100%; + border: 1px solid rgba(171, 139, 104, 0.22); + border-radius: 6px; + background: linear-gradient(180deg, rgba(171, 139, 104, 0.04), transparent 60%), rgba(17, 12, 10, 0.72); + color: #ccb292; + padding: 0.4rem 0.52rem; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); +} +.application.mgne input:focus, +.application.mgne select:focus, +.application.mgne textarea:focus { + outline: none; + border-color: rgba(221, 107, 45, 0.7); + box-shadow: 0 0 0 1px rgba(221, 107, 45, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.06); +} +.application.mgne select { + appearance: none; + padding-right: 1.3rem; + background-image: linear-gradient(45deg, transparent 50%, rgba(196, 154, 69, 0.78) 50%), linear-gradient(135deg, rgba(196, 154, 69, 0.78) 50%, transparent 50%), linear-gradient(180deg, rgba(171, 139, 104, 0.04), transparent 60%); + background-position: calc(100% - 11px) calc(50% - 2px), calc(100% - 7px) calc(50% - 2px), 0 0; + background-size: 4px 4px, 4px 4px, auto; + background-repeat: no-repeat; +} +.application.mgne textarea { + min-height: 4.5rem; + resize: vertical; +} +.application.mgne button { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + border: 1px solid rgba(196, 154, 69, 0.44); + border-radius: 999px; + background: linear-gradient(180deg, rgba(221, 107, 45, 0.2), rgba(127, 29, 23, 0.1)), linear-gradient(135deg, rgba(171, 139, 104, 0.1), transparent 40%), rgba(17, 12, 10, 0.72); + color: #ccb292; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 6px 14px rgba(0, 0, 0, 0.2); + transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease, color 120ms ease; + padding: 0.34rem 0.72rem; +} +.application.mgne button:hover, +.application.mgne button:focus { + border-color: rgba(221, 107, 45, 0.7); + color: #d9c6ae; + transform: translateY(-1px); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 8px 18px rgba(127, 29, 23, 0.26); +} +.application.mgne label { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #6e3d2a; + font-size: 0.74rem; +} +.application.mgne fieldset { + background: linear-gradient(180deg, rgba(171, 139, 104, 0.04), transparent 24%), linear-gradient(135deg, rgba(183, 70, 31, 0.08), transparent 40%), linear-gradient(180deg, rgba(27, 21, 18, 0.52), rgba(19, 16, 15, 0.34)), url("../assets/ui/page_background.webp") center center / cover no-repeat, linear-gradient(180deg, rgba(41, 30, 24, 0.88) 0%, color-mix(in srgb, rgba(41, 30, 24, 0.88) 88%, black) 100%); + border: 1px solid rgba(171, 139, 104, 0.34); + border-radius: 8px; + box-shadow: 0 14px 30px rgba(0, 0, 0, 0.36), inset 0 1px 0 rgba(255, 236, 203, 0.08), inset 0 0 0 1px rgba(255, 236, 203, 0.03); + position: relative; + margin: 0; +} +.application.mgne fieldset::before { + content: ""; + position: absolute; + inset: 6px; + pointer-events: none; + border: 1px solid rgba(196, 154, 69, 0.16); + border-radius: calc(8px - 4px); +} +.application.mgne legend { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #c49a45; + display: inline-block; + position: relative; + top: 0.12rem; + font-size: 0.82rem; + line-height: 1.1; + padding: 0.04rem 0.32rem; +} +.application.mgne code, +.mgne-chat-card code { + font-family: "Courier New", monospace; + background: rgba(19, 16, 15, 0.75); + color: #ab8b68; + padding: 0.12rem 0.35rem; + border-radius: 4px; +} +.application.mgne .empty-state { + color: rgba(95, 77, 64, 0.94); + font-style: italic; + letter-spacing: 0.03em; +} +.application.mgne .rollable { + color: #dd6b2d; + text-shadow: 0 0 12px rgba(183, 70, 31, 0.18); +} +.application.mgne .rollable:hover { + color: #e48a59; +} +#combat .combat-controls .mgne-flee-control { + display: flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + width: 100%; + min-height: 2rem; + margin-top: 0.35rem; + border: 1px solid rgba(196, 154, 69, 0.28); + border-radius: 6px; + background: linear-gradient(180deg, rgba(127, 29, 23, 0.26), rgba(183, 70, 31, 0.14)), rgba(17, 12, 10, 0.92); + color: #ab8b68; + font-family: "LoraMGNE", "Book Antiqua", serif; + font-weight: 700; +} +#combat .combat-controls .mgne-flee-control:hover:not(:disabled) { + border-color: rgba(221, 107, 45, 0.52); + color: #b69a7c; +} +#combat .combat-controls .mgne-flee-control:disabled { + opacity: 0.5; +} +.application.mgne .mgne-sheet { + display: flex; + flex-direction: column; + gap: 0.65rem; +} +.application.mgne.character .mgne-sheet-header { + flex: 0 0 auto; +} +.application.mgne .sheet-header { + display: grid; + grid-template-columns: 148px 1fr; + gap: 0.8rem; + align-items: start; +} +.application.mgne.item-sheet .sheet-header { + grid-template-columns: 111px 1fr; +} +.application.mgne .actor-portrait, +.application.mgne .item-portrait { + width: 100%; + max-width: 148px; + aspect-ratio: 1 / 1; + object-fit: cover; + border-radius: 12px; + border: 1px solid rgba(196, 154, 69, 0.4); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.34), inset 0 0 0 2px rgba(204, 178, 146, 0.08); + background: radial-gradient(circle at 30% 20%, rgba(171, 139, 104, 0.08), transparent 30%), rgba(17, 12, 10, 0.72); +} +.application.mgne.item-sheet .item-portrait { + max-width: 111px; +} +.application.mgne .header-fields, +.application.mgne .resource-bar, +.application.mgne .ability-grid, +.application.mgne .inventory-section, +.application.mgne .daily-resources-panel, +.application.mgne .tab-panel { + background: linear-gradient(180deg, rgba(171, 139, 104, 0.04), transparent 24%), linear-gradient(135deg, rgba(183, 70, 31, 0.08), transparent 40%), linear-gradient(180deg, rgba(27, 21, 18, 0.52), rgba(19, 16, 15, 0.34)), url("../assets/ui/page_background.webp") center center / cover no-repeat, linear-gradient(180deg, rgba(41, 30, 24, 0.88) 0%, color-mix(in srgb, rgba(41, 30, 24, 0.88) 88%, black) 100%); + border: 1px solid rgba(171, 139, 104, 0.34); + border-radius: 8px; + box-shadow: 0 14px 30px rgba(0, 0, 0, 0.36), inset 0 1px 0 rgba(255, 236, 203, 0.08), inset 0 0 0 1px rgba(255, 236, 203, 0.03); + position: relative; + padding: 0.65rem; +} +.application.mgne .header-fields::before, +.application.mgne .resource-bar::before, +.application.mgne .ability-grid::before, +.application.mgne .inventory-section::before, +.application.mgne .daily-resources-panel::before, +.application.mgne .tab-panel::before { + content: ""; + position: absolute; + inset: 6px; + pointer-events: none; + border: 1px solid rgba(196, 154, 69, 0.16); + border-radius: calc(8px - 4px); +} +.application.mgne .resource-bar, +.application.mgne .ability-grid, +.application.mgne .grid.two, +.application.mgne .grid.three, +.application.mgne .condition-value-grid, +.application.mgne .condition-flag-grid { + display: grid; + gap: 0.5rem; +} +.application.mgne .resource-bar { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} +.application.mgne .resource-bar-core { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.application.mgne .resource-bar-daily { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} +.application.mgne .resource-bar-equipment { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} +.application.mgne .ability-grid { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} +.application.mgne .grid.two { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.application.mgne .grid.three { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} +.application.mgne.item-sheet .item-form-grid { + display: grid; + gap: 0.65rem 0.9rem; + grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); +} +.application.mgne.item-sheet .item-form-grid-two { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.application.mgne.item-sheet .item-form-grid-three { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} +.application.mgne.item-sheet .item-form-row { + display: grid; + grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr); + align-items: center; + gap: 0.55rem; + min-width: 0; +} +.application.mgne.item-sheet .item-form-row > label { + margin: 0; + line-height: 1.1; + min-width: 0; +} +.application.mgne.item-sheet .item-form-row > input, +.application.mgne.item-sheet .item-form-row > select { + width: 100%; + min-width: 0; +} +.application.mgne .condition-value-grid { + grid-template-columns: max-content 1fr; + align-items: center; + margin-bottom: 0.6rem; +} +.application.mgne .condition-flag-grid { + grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); + gap: 0.3rem 0.6rem; +} +.application.mgne .resource-box, +.application.mgne .ability-card { + position: relative; + display: flex; + flex-direction: column; + gap: 0.28rem; + padding: 0.52rem; + border-radius: 8px; + border: 1px solid rgba(82, 69, 60, 0.3); + background: linear-gradient(180deg, rgba(171, 139, 104, 0.04), transparent 30%), linear-gradient(180deg, rgba(183, 70, 31, 0.07), transparent 80%), rgba(64, 47, 37, 0.66); +} +.application.mgne .ability-card { + min-height: 0; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 0.45rem; + padding: 0.38rem 0.46rem; +} +.application.mgne .resource-box-track, +.application.mgne .resource-box-single { + justify-content: space-between; +} +.application.mgne .resource-box-compact { + gap: 0.18rem; + padding: 0.36rem 0.42rem; +} +.application.mgne .resource-box-compact > label { + font-size: 0.68rem; + letter-spacing: 0.11em; +} +.application.mgne .resource-box-inline { + flex-direction: row; + align-items: center; + gap: 0.5rem; +} +.application.mgne .resource-box-inline > label { + flex: 0 0 auto; + margin: 0; +} +.application.mgne .resource-box-inline .resource-track, +.application.mgne .resource-box-inline .numeric-pill { + flex: 1 1 auto; +} +.application.mgne .resource-box-inline-track { + align-items: flex-start; +} +.application.mgne .resource-box-inline-track > label { + padding-top: 1rem; +} +.application.mgne .resource-box-inline-track .numeric-caption { + color: #6e3d2a; +} +.application.mgne .resource-box-inline-track .numeric-caption-strong { + padding: 0.08rem 0.34rem; + border-radius: 999px; + background: rgba(196, 154, 69, 0.18); + border: 1px solid rgba(110, 61, 42, 0.28); + color: #5f3524; + font-size: 0.6rem; + letter-spacing: 0.11em; +} +.application.mgne .resource-box-inline-track .numeric-cluster { + align-items: center; +} +.application.mgne .resource-box-inline-track .numeric-input { + width: 3.4rem; + min-width: 3.4rem; +} +.application.mgne .resource-box-inline-single .numeric-pill { + flex: 0 0 auto; +} +.application.mgne .resource-box-inline-single .numeric-input { + width: 3.2rem; + min-width: 3.2rem; +} +.application.mgne .header-resource { + align-self: stretch; +} +.application.mgne .header-resource .resource-box { + height: 100%; +} +.application.mgne .resource-track { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 0.32rem; + align-items: end; +} +.application.mgne .resource-track-die { + grid-template-columns: 1fr 1fr; +} +.application.mgne .numeric-cluster { + display: flex; + flex-direction: column; + gap: 0.18rem; +} +.application.mgne .numeric-caption { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(129, 83, 59, 0.94); + font-size: 0.58rem; + letter-spacing: 0.12em; +} +.application.mgne .resource-box-compact .numeric-caption { + font-size: 0.52rem; + letter-spacing: 0.09em; +} +.application.mgne .track-separator { + align-self: center; + color: rgba(196, 154, 69, 0.72); + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + font-size: 1rem; + line-height: 1; +} +.application.mgne .numeric-input { + min-width: 0; + text-align: center; + text-align-last: center; + font-variant-numeric: tabular-nums; + font-weight: 700; + font-size: 1rem; + padding-inline: 0.2rem; +} +.application.mgne .resource-box-compact .numeric-input, +.application.mgne .resource-box-compact .compact-select { + min-height: 30px; + padding: 0.18rem 0.16rem; + font-size: 0.9rem; +} +.application.mgne .numeric-input-readonly { + color: #ab8b68; +} +.application.mgne .compact-select { + text-align: center; + text-align-last: center; + font-variant-numeric: tabular-nums; + font-weight: 700; + padding-inline: 0.35rem; +} +.application.mgne .numeric-pill { + display: flex; + align-items: center; + justify-content: center; + padding: 0.18rem; + border-radius: 8px; + border: 1px solid rgba(196, 154, 69, 0.18); + background: rgba(17, 12, 10, 0.64); +} +.application.mgne .resource-box-compact .numeric-pill { + padding: 0.1rem; +} +.application.mgne .numeric-pill-suffix { + gap: 0.35rem; + justify-content: space-between; +} +.application.mgne .numeric-suffix { + flex: 0 0 auto; + color: #c49a45; + font-weight: 700; + font-size: 1rem; + line-height: 1; +} +.application.mgne .resource-meta { + display: inline-flex; + align-self: flex-start; + margin-top: 0.12rem; + padding: 0.16rem 0.44rem; + border-radius: 999px; + border: 1px solid rgba(79, 125, 115, 0.2); + color: #7caca2; + background: rgba(79, 125, 115, 0.1); + font-size: 0.72rem; +} +.application.mgne .resource-box-compact .resource-track { + gap: 0.22rem; +} +.application.mgne .resource-box-compact .numeric-cluster { + gap: 0.1rem; +} +.application.mgne .resource-box-compact .track-separator { + font-size: 0.88rem; +} +.application.mgne .resource-box-compact .resource-meta { + margin-top: 0.06rem; + padding: 0.1rem 0.34rem; + font-size: 0.64rem; +} +.application.mgne .resource-inline { + display: flex; + gap: 0.32rem; + align-items: center; +} +.application.mgne .resource-inline span { + color: #7e664f; +} +.application.mgne .sheet-tabs { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + flex: 0 0 auto; + justify-content: center; +} +.application.mgne .tab-button { + min-width: 110px; +} +.application.mgne .tab-button.active { + border-color: rgba(221, 107, 45, 0.7); + background: linear-gradient(180deg, rgba(221, 107, 45, 0.26), rgba(127, 29, 23, 0.12)), linear-gradient(135deg, rgba(204, 178, 146, 0.12), transparent 45%), rgba(17, 12, 10, 0.72); + color: #d6c1a7; +} +.application.mgne .tab-panel:not(.active) { + display: none; +} +.application.mgne.character .tab-panel.active { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; +} +.application.mgne .inventory-section { + display: flex; + flex-direction: column; + gap: 0.4rem; +} +.application.mgne .daily-resources-panel { + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.application.mgne .section-heading { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 0.5rem; + flex-wrap: wrap; +} +.application.mgne .section-heading h2 { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + margin: 0; + color: #ab8b68; + font-size: 0.9rem; +} +.application.mgne .section-heading small { + display: inline-block; + margin-top: 0.12rem; + color: #7e664f; + font-size: 0.72rem; +} +.application.mgne .inventory-header, +.application.mgne .inline-buttons, +.application.mgne .item-actions, +.application.mgne .sheet-actions { + display: flex; + gap: 0.35rem; + align-items: center; + flex-wrap: wrap; +} +.application.mgne .resource-box-actions { + display: flex; + justify-content: flex-start; + margin-top: 0.12rem; +} +.application.mgne .resource-box-actions-rest { + gap: 0.32rem; + flex-wrap: wrap; +} +.application.mgne .resource-box-actions-rest > button { + flex: 1 1 0; + min-width: 0; + padding-inline: 0.5rem; + white-space: nowrap; +} +.application.mgne .inventory-header { + justify-content: space-between; +} +.application.mgne .inventory-header h3 { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + margin: 0; + color: #ab8b68; + font-size: 0.82rem; +} +.application.mgne .item-row { + position: relative; + display: grid; + grid-template-columns: 2fr 1fr 1fr auto; + gap: 0.45rem; + align-items: center; + padding: 0.48rem 0.2rem 0.48rem 0.55rem; + border-radius: 6px; + border: 1px solid transparent; + background: linear-gradient(90deg, rgba(183, 70, 31, 0.09), transparent 35%), linear-gradient(180deg, rgba(204, 178, 146, 0.02), rgba(19, 16, 15, 0.12)), rgba(17, 12, 10, 0.8); +} +.application.mgne .item-row:hover { + border-color: rgba(196, 154, 69, 0.28); + background: linear-gradient(90deg, rgba(183, 70, 31, 0.12), transparent 35%), linear-gradient(180deg, rgba(204, 178, 146, 0.04), rgba(19, 16, 15, 0.12)), rgba(17, 12, 10, 0.88); +} +.application.mgne .item-name { + color: #ccb292; + font-weight: 700; + letter-spacing: 0.02em; + font-size: 0.95rem; +} +.application.mgne .check-grid { + display: flex; + gap: 0.7rem; + flex-wrap: wrap; +} +.application.mgne.item-sheet .item-check-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); + gap: 0.45rem 0.9rem; + align-items: center; +} +.application.mgne.item-sheet .check-grid > label { + display: inline-flex; + align-items: center; + gap: 0.35rem; + flex: 0 0 auto; + width: fit-content; + margin: 0; +} +.application.mgne .checkbox-line { + display: inline-flex; + gap: 0.35rem; + align-items: center; + width: fit-content; +} +.application.mgne .checkbox-line.active { + color: #dd6b2d; + font-weight: 600; +} +.application.mgne .check-grid input[type="checkbox"], +.application.mgne .checkbox-line input[type="checkbox"] { + --checkbox-size: 0.9rem; + flex: 0 0 0.9rem; + width: 0.9rem; + height: 0.9rem; + min-width: 0.9rem; + padding: 0; + margin: 0; + position: relative; +} +@media (max-width: 820px) { + .application.mgne.item-sheet .item-form-grid-two, + .application.mgne.item-sheet .item-form-grid-three { + grid-template-columns: minmax(0, 1fr); + } +} +.application.mgne .ability-label { + display: inline-flex; + flex: 1 1 auto; + align-items: center; + justify-content: flex-start; + min-width: 0; +} +.application.mgne .resource-label-accent { + color: #dd6b2d; + text-shadow: 0 0 12px rgba(183, 70, 31, 0.18); +} +.application.mgne .ability-score { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 58px; + min-height: 32px; + padding: 0.08rem; + border-radius: 6px; + border: 1px solid rgba(196, 154, 69, 0.14); + background: rgba(17, 12, 10, 0.54); +} +.application.mgne .ability-input { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + font-size: 1.05rem; + line-height: 1; + color: #ab8b68; + padding: 0.12rem; +} +.application.mgne select.ability-input { + flex: 1 1 auto; + width: 100%; +} +.application.mgne .ability-score-text { + flex-direction: column; + gap: 0.02rem; +} +.application.mgne .ability-defense-main { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #ab8b68; + font-size: 0.8rem; +} +.application.mgne .ability-defense-sub { + color: #7e664f; + font-size: 0.62rem; +} +.application.mgne .character .ability-card:nth-child(odd), +.application.mgne .character .resource-box:nth-child(odd) { + background: linear-gradient(180deg, rgba(79, 125, 115, 0.1), transparent 55%), linear-gradient(180deg, rgba(204, 178, 146, 0.04), transparent 35%), rgba(64, 47, 37, 0.66); +} +.application.mgne .creature .resource-box, +.application.mgne .companion .resource-box, +.application.mgne .creature .ability-card, +.application.mgne .companion .ability-card { + background: linear-gradient(180deg, rgba(127, 29, 23, 0.11), transparent 60%), linear-gradient(180deg, rgba(204, 178, 146, 0.04), transparent 35%), rgba(64, 47, 37, 0.66); +} +@media (max-width: 960px) { + .application.mgne .sheet-header, + .application.mgne .resource-bar, + .application.mgne .ability-grid, + .application.mgne .grid.two, + .application.mgne .grid.three { + grid-template-columns: 1fr; + } + .application.mgne .item-row { + grid-template-columns: 1fr; + } +} +.application.mgne.roll-dialog .window-content { + padding: 0.55rem; + background: radial-gradient(circle at top left, rgba(221, 107, 45, 0.14), transparent 26%), linear-gradient(180deg, rgba(27, 21, 18, 0.26), rgba(19, 16, 15, 0.22)), url("../assets/ui/page_background.webp") center center / cover no-repeat, linear-gradient(180deg, #1b1512, #13100f 120%); +} +.application.mgne.roll-dialog .dialog-content { + padding: 0; +} +.application.mgne.roll-dialog .mgne-roll-dialog { + background: linear-gradient(135deg, rgba(183, 70, 31, 0.1), transparent 32%), linear-gradient(180deg, rgba(27, 21, 18, 0.94), rgba(19, 16, 15, 0.98)), #13100f; + border: 1px solid rgba(196, 154, 69, 0.28); + border-radius: 6px; + box-shadow: 0 14px 30px rgba(0, 0, 0, 0.36), inset 0 1px 0 rgba(255, 236, 203, 0.08), inset 0 0 0 1px rgba(255, 236, 203, 0.03); + position: relative; + display: flex; + flex-direction: column; + gap: 0.55rem; + padding: 0.7rem; +} +.application.mgne.roll-dialog .mgne-roll-dialog::before { + content: ""; + position: absolute; + inset: 6px; + pointer-events: none; + border: 1px solid rgba(196, 154, 69, 0.16); + border-radius: calc(8px - 4px); +} +.application.mgne.roll-dialog .mgne-roll-dialog > p:first-child { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.9rem; + color: #c49a45; + margin: 0; +} +.application.mgne.roll-dialog .mgne-roll-dialog > p:nth-child(2) { + margin: 0; + color: #ab8b68; + font-style: italic; +} +.application.mgne.roll-dialog .mgne-roll-dialog label { + color: #ccb292; +} +.application.mgne.roll-dialog .mgne-roll-dialog strong { + color: #ccb292; +} +.application.mgne.roll-dialog .mgne-roll-dialog label, +.application.mgne.roll-dialog .mgne-roll-dialog .checkbox-line { + color: #ccb292; +} +.application.mgne.roll-dialog .form-footer button { + min-width: 132px; +} +.mgne-chat-card { + background: linear-gradient(180deg, rgba(171, 139, 104, 0.04), transparent 24%), linear-gradient(135deg, rgba(183, 70, 31, 0.08), transparent 40%), linear-gradient(180deg, rgba(27, 21, 18, 0.52), rgba(19, 16, 15, 0.34)), url("../assets/ui/page_background.webp") center center / cover no-repeat, linear-gradient(180deg, rgba(41, 30, 24, 0.88) 0%, color-mix(in srgb, rgba(41, 30, 24, 0.88) 88%, black) 100%); + border: 1px solid rgba(171, 139, 104, 0.34); + border-radius: 8px; + box-shadow: 0 14px 30px rgba(0, 0, 0, 0.36), inset 0 1px 0 rgba(255, 236, 203, 0.08), inset 0 0 0 1px rgba(255, 236, 203, 0.03); + position: relative; + display: flex; + flex-direction: column; + gap: 0.6rem; + padding: 0.68rem; + color: #52453c; + background: radial-gradient(circle at top right, rgba(79, 125, 115, 0.1), transparent 22%), linear-gradient(135deg, rgba(183, 70, 31, 0.1), transparent 38%), linear-gradient(180deg, rgba(171, 139, 104, 0.04), transparent 24%), linear-gradient(180deg, rgba(27, 21, 18, 0.22), rgba(19, 16, 15, 0.18)), url("../assets/ui/page_background.webp") right bottom / cover no-repeat, rgba(41, 30, 24, 0.88); +} +.mgne-chat-card::before { + content: ""; + position: absolute; + inset: 6px; + pointer-events: none; + border: 1px solid rgba(196, 154, 69, 0.16); + border-radius: calc(8px - 4px); +} +.mgne-chat-card .chat-card-header { + display: flex; + gap: 0.6rem; + align-items: center; +} +.mgne-chat-card .chat-card-header img { + width: 36px; + height: 36px; + object-fit: cover; + border-radius: 50%; + border: 1px solid rgba(196, 154, 69, 0.38); + box-shadow: 0 0 0 2px rgba(204, 178, 146, 0.06); +} +.mgne-chat-card .chat-eyebrow { + display: none; +} +.mgne-chat-card h3 { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + margin: 0; + color: #6e3d2a; + font-size: 0.82rem; +} +.mgne-chat-card .chat-actor, +.mgne-chat-card .chat-subtitle, +.mgne-chat-card .chat-formula, +.mgne-chat-card .chat-special { + margin: 0; +} +.mgne-chat-card .chat-actor { + color: #52453c; +} +.mgne-chat-card .chat-formula { + color: #52453c; +} +.mgne-chat-card .chat-formula code { + color: #ccb292; + background: rgba(19, 16, 15, 0.75); + padding: 0.1em 0.42em; + border-radius: 3px; +} +.mgne-chat-card .chat-result-line { + display: flex; + justify-content: space-between; + align-items: end; + padding: 0.38rem 0.55rem; + border: 1px solid rgba(196, 154, 69, 0.28); + border-radius: 6px; + background: rgba(19, 16, 15, 0.78); +} +.mgne-chat-card .chat-result-label { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.62rem; + color: #ccb292; +} +.mgne-chat-card .chat-result-total { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + font-size: 1.08rem; + color: #ccb292; +} +.mgne-chat-card .chat-outcome { + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + text-transform: uppercase; + letter-spacing: 0.08em; + margin: 0; + padding: 0.3rem 0.55rem; + border-radius: 6px; + background: rgba(19, 16, 15, 0.72); + border-left: 3px solid rgba(221, 107, 45, 0.7); + color: #e69062; + font-size: 0.8rem; +} +.mgne-chat-card .chat-special { + padding: 0.42rem 0.55rem; + border-left: 3px solid rgba(221, 107, 45, 0.55); + background: rgba(17, 12, 10, 0.85); + color: #ccb292; +} +.mgne-chat-card.outcome-critical-success, +.mgne-chat-card.outcome-success, +.mgne-chat-card.outcome-steady { + border-color: rgba(79, 125, 115, 0.42); +} +.mgne-chat-card.outcome-critical-success .chat-outcome, +.mgne-chat-card.outcome-success .chat-outcome, +.mgne-chat-card.outcome-steady .chat-outcome { + color: #9bc0b8; + border-left-color: rgba(79, 125, 115, 0.8); +} +.mgne-chat-card.outcome-failure, +.mgne-chat-card.outcome-broken, +.mgne-chat-card.outcome-fumble { + border-color: rgba(127, 29, 23, 0.48); +} +.mgne-chat-card.outcome-failure .chat-outcome, +.mgne-chat-card.outcome-broken .chat-outcome, +.mgne-chat-card.outcome-fumble .chat-outcome { + color: #eaa37c; + border-left-color: rgba(221, 107, 45, 0.8); +} +.mgne-chat-card.mode-apply-damage .chat-result-total, +.mgne-chat-card.mode-damage .chat-result-total { + color: #e38450; +} +.mgne-chat-card .chat-card-actions { + margin-top: 0.2rem; +} +.mgne-roll-damage-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + width: 100%; + padding: 0.38rem 0.7rem; + border: 1px solid #dd6b2d; + border-radius: 6px; + background: #b7461f; + color: #f8ede0; + font-family: "CastorTwoMGNE", "Palatino Linotype", serif; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.06em; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} +.mgne-roll-damage-btn .dmg-formula { + color: #fff5e8; + font-size: 0.75rem; + font-weight: 700; +} +.mgne-roll-damage-btn:hover { + background: #da5325; + border-color: #e69062; + color: #fff; +} +.mgne-roll-damage-btn.is-critical { + border: 1px solid #c49a45; + background: #a27e34; + color: #fff5d0; + font-weight: 700; +} +.mgne-roll-damage-btn.is-critical .dmg-formula { + color: #fff; +} +.mgne-roll-damage-btn.is-critical:hover { + background: #c49a45; + color: #fff; +} +.chat-apply-actions { + margin-top: 0.35rem; +} +.mgne-apply-damage-select { + width: 100%; + padding: 0.35rem 0.6rem; + border: 1px solid #dd6b2d; + border-radius: 6px; + background: #1b1512; + color: #ab8b68; + font-family: "LoraMGNE", "Book Antiqua", serif; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} +.mgne-apply-damage-select option { + background: #1b1512; + color: #ab8b68; +} +.mgne-apply-damage-select:hover { + border-color: #e69062; + background: rgba(183, 70, 31, 0.18); +} +.mgne-apply-damage-select:focus { + outline: none; + border-color: #dd6b2d; +} diff --git a/fvtt-machine-gods-noxian-expanse.mjs b/fvtt-machine-gods-noxian-expanse.mjs new file mode 100644 index 0000000..6d524e0 --- /dev/null +++ b/fvtt-machine-gods-noxian-expanse.mjs @@ -0,0 +1,148 @@ +import { ASCII, SYSTEM, SYSTEM_ID, localizeSystemConfig } from "./module/config/system.mjs" +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 MGNERoll from "./module/documents/roll.mjs" + +Hooks.once("init", () => { + console.info(ASCII) + console.info(`${SYSTEM_ID} | Initializing system`) + + game.mgne = { + SYSTEM, + applications, + documents, + models, + } + + CONFIG.Actor.documentClass = documents.MGNEActor + CONFIG.Actor.dataModels = { + character: models.MGNECharacter, + creature: models.MGNECreature, + companion: models.MGNECompanion, + } + + CONFIG.Combat.documentClass = documents.MGNECombat + CONFIG.Item.documentClass = documents.MGNEItem + CONFIG.Item.dataModels = { + weapon: models.MGNEWeapon, + armor: models.MGNEArmor, + shield: models.MGNEShield, + equipment: models.MGNEEquipment, + "resonance-core": models.MGNEResonanceCore, + artifact: models.MGNEArtifact, + feature: models.MGNEFeature, + } + + foundry.applications.sheets.ActorSheetV2 && foundry.documents.collections.Actors.unregisterSheet( + "core", + foundry.applications.sheets.ActorSheetV2, + { types: Object.keys(CONFIG.Actor.dataModels) } + ) + foundry.appv1?.sheets?.ActorSheet && foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet) + foundry.documents.collections.Actors.registerSheet(SYSTEM_ID, applications.MGNECharacterSheet, { types: ["character"], makeDefault: true, label: SYSTEM.actorTypes.character.label }) + foundry.documents.collections.Actors.registerSheet(SYSTEM_ID, applications.MGNECreatureSheet, { types: ["creature"], makeDefault: true, label: SYSTEM.actorTypes.creature.label }) + foundry.documents.collections.Actors.registerSheet(SYSTEM_ID, applications.MGNECompanionSheet, { types: ["companion"], makeDefault: true, label: SYSTEM.actorTypes.companion.label }) + + foundry.applications.sheets.ItemSheetV2 && foundry.documents.collections.Items.unregisterSheet( + "core", + foundry.applications.sheets.ItemSheetV2, + { types: Object.keys(CONFIG.Item.dataModels) } + ) + foundry.appv1?.sheets?.ItemSheet && foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet) + foundry.documents.collections.Items.registerSheet(SYSTEM_ID, applications.MGNEWeaponSheet, { types: ["weapon"], makeDefault: true, label: SYSTEM.itemTypes.weapon.label }) + foundry.documents.collections.Items.registerSheet(SYSTEM_ID, applications.MGNEArmorSheet, { types: ["armor"], makeDefault: true, label: SYSTEM.itemTypes.armor.label }) + foundry.documents.collections.Items.registerSheet(SYSTEM_ID, applications.MGNEShieldSheet, { types: ["shield"], makeDefault: true, label: SYSTEM.itemTypes.shield.label }) + foundry.documents.collections.Items.registerSheet(SYSTEM_ID, applications.MGNEEquipmentSheet, { types: ["equipment"], makeDefault: true, label: SYSTEM.itemTypes.equipment.label }) + foundry.documents.collections.Items.registerSheet(SYSTEM_ID, applications.MGNEResonanceCoreSheet, { types: ["resonance-core"], makeDefault: true, label: SYSTEM.itemTypes["resonance-core"].label }) + foundry.documents.collections.Items.registerSheet(SYSTEM_ID, applications.MGNEArtifactSheet, { types: ["artifact"], makeDefault: true, label: SYSTEM.itemTypes.artifact.label }) + foundry.documents.collections.Items.registerSheet(SYSTEM_ID, applications.MGNEFeatureSheet, { types: ["feature"], makeDefault: true, label: SYSTEM.itemTypes.feature.label }) + + Handlebars.registerHelper("isEqual", (left, right) => left === right) +}) + +Hooks.once("setup", () => { + localizeSystemConfig() +}) + +Hooks.once("ready", () => { + console.info(`${SYSTEM_ID} | Ready`) +}) + +Hooks.on("renderCombatTracker", (_app, element) => { + const root = element instanceof HTMLElement ? element : element?.[0] + if (!root) return + + const footer = root.querySelector(".combat-controls") + if (!footer || footer.querySelector(".mgne-flee-control")) return + + const button = document.createElement("button") + button.type = "button" + button.className = "combat-control-lg mgne-flee-control" + button.dataset.action = "mgneFlee" + button.innerHTML = `${game.i18n.localize("MGNE.Combat.Flee")}` + button.disabled = !game.combat + button.addEventListener("click", event => { + event.preventDefault() + game.combat?.rollFlee() + }) + + footer.append(button) +}) + +Hooks.on("renderChatMessageHTML", (message, element) => { + const root = element instanceof HTMLElement ? element : element?.[0] + if (!root) return + + root.querySelectorAll(".mgne-roll-damage-btn").forEach(btn => { + btn.addEventListener("click", async () => { + const actorId = btn.dataset.actorId + const itemId = btn.dataset.itemId + const actor = game.actors.get(actorId) + const item = actor?.items.get(itemId) + if (!actor || !item) { + ui.notifications.warn(game.i18n.localize("MGNE.Notification.ActorOrItemNotFound")) + return + } + await MGNERoll.rollDamage({ actor, item }) + }) + }) + + root.querySelectorAll(".mgne-apply-damage-select").forEach(select => { + const isAllowed = game.user.isGM || message.isAuthor + if (!isAllowed) { + select.closest(".chat-apply-actions")?.remove() + return + } + + const card = select.closest(".mgne-chat-card") + const damageTotal = parseInt(card?.dataset.damageTotal ?? "0", 10) || 0 + const damageCritical = card?.dataset.damageCritical === "true" + + const tokens = canvas.scene?.tokens.contents ?? [] + for (const token of tokens) { + if (!token.actor) continue + const opt = document.createElement("option") + opt.value = token.id + opt.textContent = token.name + select.appendChild(opt) + } + + if (tokens.length === 0) { + const opt = document.createElement("option") + opt.disabled = true + opt.textContent = game.i18n.localize("MGNE.Roll.NoTargetSelected") + select.appendChild(opt) + } + + select.addEventListener("change", async event => { + const tokenId = event.target.value + if (!tokenId) return + const token = canvas.scene?.tokens.get(tokenId) + const targetActor = token?.actor + if (!targetActor) return + select.value = "" + await targetActor.applyDamage(damageTotal, { critical: damageCritical, chat: true }) + }) + }) +}) diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..eb454ce --- /dev/null +++ b/lang/en.json @@ -0,0 +1,691 @@ +{ + "MGNE": { + "SystemName": "Machine Gods of the Noxian Expanse", + "ActorTypes": { + "character": "Character", + "creature": "Creature", + "companion": "Companion" + }, + "ItemTypes": { + "weapon": "Weapon", + "armor": "Armor", + "shield": "Shield", + "equipment": "Equipment", + "resonance-core": "Resonance Core", + "artifact": "Artifact", + "feature": "Feature" + }, + "Abilities": { + "agility": "Agility", + "presence": "Presence", + "strength": "Strength", + "toughness": "Toughness" + }, + "Conditions": { + "bleeding": "Bleeding", + "blinded": "Blinded", + "burning": "Burning", + "fatigued": "Fatigued", + "infected": "Infected", + "poisoned": "Poisoned", + "prone": "Prone", + "restrained": "Restrained", + "starved": "Starved", + "stunned": "Stunned" + }, + "WeaponCategories": { + "melee": "Melee", + "ranged": "Ranged" + }, + "Resonations": { + "accelerate": "Accelerate", + "blast": "Blast", + "breathe-water": "Breathe Water", + "cauterize": "Cauterize", + "create-illusion": "Create Illusion", + "distract": "Distract", + "eagle-eye": "Eagle Eye", + "empower-weapon": "Empower Weapon", + "fireball": "Fireball", + "hover": "Hover", + "influence-mind": "Influence Mind", + "knit-flesh": "Knit Flesh", + "light-construct": "Light Construct", + "mirage": "Mirage", + "negate-injury": "Negate Injury", + "paralyze": "Paralyze", + "shield": "Shield", + "shock": "Shock", + "shroud": "Shroud", + "summon-mist": "Summon Mist" + }, + "EquipmentSubtypes": { + "gear": "Gear", + "consumable": "Consumable", + "travel": "Travel", + "container": "Container", + "tool": "Tool" + }, + "Tabs": { + "overview": "Overview", + "daily": "Daily Resources", + "equipment": "Equipment", + "features": "Features", + "notes": "Notes" + }, + "Character": { + "Background": "Background", + "Origin": "Origin", + "Scars": "Scars", + "Motivation": "Motivation", + "Vice": "Vice", + "QuickRest": "Quick Rest", + "QuickRestHelp": "5 minutes, 1 ration, and a swig of water.", + "FullRest": "Full Rest", + "FullRestHelp": "6 hours, 1 ration, and a relatively safe environment.", + "ResetDailyResources": "Reset Daily Resources", + "Omens": "Omens", + "Resonations": "Resonations", + "Remaining": "Remaining", + "ResonancePerDay": "Resonance per Day", + "ArtifactSync": "Artifact Sync", + "CarryingCapacity": "Carrying Capacity", + "Rations": "Rations", + "Kiffol": "Kiffol", + "Weapons": "Weapons", + "AddWeapon": "Add Weapon", + "Armor": "Armor", + "AddArmor": "Add Armor", + "AddShield": "Add Shield", + "AddEquipment": "Add Equipment", + "ResonanceCores": "Resonance Cores", + "AddCore": "Add Core", + "Artifacts": "Artifacts", + "AddArtifact": "Add Artifact", + "AddFeature": "Add Feature", + "RulesSnapshot": "Rules Snapshot", + "FIELDS": { + "abilities": { + "label": "Abilities", + "agility": { + "label": "Agility" + }, + "presence": { + "label": "Presence" + }, + "strength": { + "label": "Strength" + }, + "toughness": { + "label": "Toughness" + } + }, + "hp": { + "label": "HP", + "value": { + "label": "Current" + }, + "max": { + "label": "Max" + } + }, + "omens": { + "label": "Omens", + "current": { + "label": "Current" + }, + "die": { + "label": "Die" + } + }, + "resonance": { + "label": "Resonance", + "max": { + "label": "Max" + }, + "used": { + "label": "Used" + }, + "blocked": { + "label": "Blocked" + } + }, + "artifactSync": { + "label": "Artifact Sync", + "used": { + "label": "Used" + } + }, + "survival": { + "label": "Survival", + "salvationUsed": { + "label": "Salvation Used" + } + }, + "conditions": { + "label": "Conditions", + "bleeding": { + "label": "Bleeding" + }, + "blinded": { + "label": "Blinded" + }, + "burning": { + "label": "Burning" + }, + "fatigued": { + "label": "Fatigued" + }, + "infected": { + "label": "Infected" + }, + "poisoned": { + "label": "Poisoned" + }, + "prone": { + "label": "Prone" + }, + "restrained": { + "label": "Restrained" + }, + "starved": { + "label": "Starved" + }, + "stunned": { + "label": "Stunned" + } + }, + "carryCapacity": { + "label": "Carrying Capacity" + }, + "rations": { + "label": "Rations" + }, + "kiffol": { + "label": "Kiffol" + }, + "background": { + "label": "Background" + }, + "origin": { + "label": "Origin" + }, + "scars": { + "label": "Scars" + }, + "motivation": { + "label": "Motivation" + }, + "vice": { + "label": "Vice" + }, + "description": { + "label": "Description" + }, + "notes": { + "label": "Notes" + } + } + }, + "Creature": { + "Special": "Special", + "FIELDS": { + "abilities": { + "label": "Abilities", + "agility": { + "label": "Agility" + }, + "presence": { + "label": "Presence" + }, + "strength": { + "label": "Strength" + }, + "toughness": { + "label": "Toughness" + } + }, + "hp": { + "label": "HP", + "value": { + "label": "Current" + }, + "max": { + "label": "Max" + } + }, + "morale": { + "label": "Morale" + }, + "armor": { + "label": "Armor", + "die": { + "label": "Armor Die" + } + }, + "attack": { + "label": "Attack", + "damage": { + "label": "Damage" + } + }, + "description": { + "label": "Description" + }, + "special": { + "label": "Special" + }, + "notes": { + "label": "Notes" + } + } + }, + "Companion": { + "TheyValue": "They Value...", + "Trait": "Trait", + "Specialty": "Specialty", + "AdventuringBehavior": "Adventuring Behavior", + "CombatBehavior": "Combat Behavior", + "FIELDS": { + "abilities": { + "label": "Abilities", + "agility": { + "label": "Agility" + }, + "presence": { + "label": "Presence" + }, + "strength": { + "label": "Strength" + }, + "toughness": { + "label": "Toughness" + } + }, + "hp": { + "label": "HP", + "value": { + "label": "Current" + }, + "max": { + "label": "Max" + } + }, + "morale": { + "label": "Morale" + }, + "armor": { + "label": "Armor", + "die": { + "label": "Armor Die" + } + }, + "attack": { + "label": "Attack", + "damage": { + "label": "Damage" + } + }, + "valueText": { + "label": "They Value..." + }, + "traitText": { + "label": "Trait" + }, + "specialtyText": { + "label": "Specialty" + }, + "adventuringBehavior": { + "label": "Adventuring Behavior" + }, + "combatBehavior": { + "label": "Combat Behavior" + }, + "upkeep": { + "label": "Upkeep" + }, + "description": { + "label": "Description" + }, + "notes": { + "label": "Notes" + } + } + }, + "DataModel": { + "abilities": { + "agility": { + "label": "Agility" + }, + "presence": { + "label": "Presence" + }, + "strength": { + "label": "Strength" + }, + "toughness": { + "label": "Toughness" + } + } + }, + "Common": { + "Attack": "Attack", + "ArmorDie": "Armor Die", + "ArtifactId": "Artifact Id", + "Broken": "Broken", + "BurnedOut": "Burned Out", + "Carried": "Carried", + "Category": "Category", + "Conditions": "Conditions", + "Consumable": "Consumable", + "Current": "Current", + "Damage": "Damage", + "Depleted": "Depleted", + "Defense": "Defense", + "Delete": "Delete", + "Description": "Description", + "Die": "Die", + "Edit": "Edit", + "Equip": "Equip", + "Equipped": "Equipped", + "FeatureId": "Feature Id", + "HP": "HP", + "Invoke": "Invoke", + "Label": "Label", + "Limit": "Limit", + "Max": "Max", + "Morale": "Morale", + "NewItem": "New {type}", + "Notes": "Notes", + "Penalty": "Penalty", + "Properties": "Properties", + "Quantity": "Quantity", + "QuantityShort": "Qty", + "Range": "Range", + "Resonation": "Resonation", + "Roll": "Roll", + "Subtype": "Subtype", + "Sync": "Sync", + "Desync": "Desync", + "Synchronized": "Synchronized", + "SynchronizedTo": "Synchronized To", + "Unequip": "Unequip", + "Unsynchronized": "Unsynchronized", + "Usage": "Usage", + "UsageDie": "Usage Die", + "Used": "Used", + "Value": "Value" + }, + "Empty": { + "NoArtifacts": "No artifacts yet.", + "NoEquipment": "No equipment yet.", + "NoFeatures": "No features yet.", + "NoResonanceCores": "No resonance cores yet.", + "NoWeapons": "No weapons yet." + }, + "RulesSnapshot": { + "Checks": "Checks use d20 + ability vs DR.", + "Attacks": "Melee attacks use Strength, ranged attacks use Presence, defense uses Agility, resonations use Presence.", + "Armor": "Armor is rolled automatically when damage is applied.", + "Breaks": "At 0 HP a character Breaks.", + "Morale": "Morale checks use 2d6 and break on score or higher." + }, + "RollDialog": { + "DR": "DR", + "Modifier": "Modifier", + "SpendOmen": "Spend 1 omen to lower DR by 4", + "VsDR": "vs DR" + }, + "Initiative": { + "SideRoll": "Initiative side roll: {roll}.", + "PlayersFirst": "Players act first.", + "EnemiesFirst": "Enemies act first.", + "TieBreak": "Use 1d6 + Agility to decide who acts next." + }, + "Combat": { + "Flee": "Fleeing", + "FleeNoCombatant": "Select or activate a combatant before resolving fleeing.", + "StudyActions": "Actions spent studying the battlefield", + "StudyHelp": "Each action lowers the target number by 1, to a minimum of 2+." + }, + "Notification": { + "ActorOrItemNotFound": "Actor or item not found for damage roll.", + "CannotSyncMore": "{actor} cannot synchronize any more artifacts today.", + "ItemBroken": "{item} is broken.", + "ItemBurnedOut": "{item} is burned out.", + "ItemDepleted": "{item} is already depleted.", + "ResonancePerDayReached": "{actor} has already used all resonations for today." + }, + "Roll": { + "AppliedDamage": "Applied {amount}", + "ApplyTo": "Apply to...", + "ApplyDamageTo": "Apply {amount} damage to {target}", + "AppliedDamageText": "Applied damage: {amount}.", + "ArmorAbsorbed": "Armor absorbed {amount}.", + "ArmorDegradedCritical": "Critical: {item} armor downgraded to {die}.", + "ArmorNothingToDegrade": "Critical: no armor to downgrade.", + "AttackFumble": "Attack fumble: the weapon breaks.", + "BreakText": "Break: {text}", + "CheckLabel": "{ability} Check", + "CheckSubtitle": "{ability} vs DR {dr}", + "CriticalAttack": "Critical attack: your next damage roll for this attack is doubled, and the target's armor is downgraded by one step.", + "CriticalDamageApplied": "Critical damage applied.", + "CurrentDie": "Current die {die}", + "DamageSourceItem": "{item}", + "DamageSourceWithActor": "{item} from {actor}", + "DefenseCheck": "Defense Check", + "DefenseCritical": "Defense crit: you may make an immediate counterattack.", + "DefenseFumble": "Defense fumble: the next damage you take is doubled, then your armor is downgraded by one step.", + "DefenseFumbleApplied": "Defense fumble: {item} is downgraded to {die}.", + "DefenseFumbleNoArmor": "Defense fumble: no armor remained to downgrade.", + "DirectDamage": "Direct damage", + "DowngradedTo": "Downgraded to {die}", + "FleeEscaped": "Escaped", + "FleeKilled": "Caught and killed", + "FleeLabel": "{actor} Attempts to Flee", + "FleeNoStudyActions": "No time was spent studying the battlefield.", + "FleeStudyActions": "Studied the battlefield for {count} action(s).", + "FleeSubtitle": "Escape on {target}+", + "FullRestLabel": "{actor} Takes a Full Rest", + "HPNow": "HP now {hp}", + "HPNowMax": "HP now {hp}/{max}.", + "InvocationLabel": "{item} Invocation", + "ItemAttackLabel": "{item} Attack", + "ItemDamageLabel": "{item} Damage", + "ItemNowDepleted": "The item is depleted.", + "ItemUsageLabel": "{item} Usage", + "MoraleBrokenText": "The actor breaks down and may flee, surrender, or grant +d4 damage to opponents.", + "MoraleCheck": "Morale Check", + "NoChange": "No change", + "NoTargetSelected": "No target selected", + "OmensReset": "Omens reset to {omens} ({die}: {roll}).", + "OutcomeBroken": "Broken", + "OutcomeCriticalSuccess": "Critical Success", + "OutcomeFailure": "Failure", + "OutcomeFumble": "Fumble", + "OutcomeRolled": "Rolled", + "OutcomeSteady": "Steady", + "OutcomeSuccess": "Success", + "QuickRestLabel": "{actor} Takes a Quick Rest", + "RestoredHP": "Restored {amount} HP", + "RollDamage": "Roll Damage", + "TakesDamageLabel": "{actor} Takes Damage", + "TargetName": "Target: {target}", + "TargetSubtitle": "Target {target}" + }, + "Chat": { + "DamageSummary": "Absorbed {absorbed}, applied {appliedDamage}, HP now {hp}.", + "Formula": "Formula", + "Result": "Result", + "Mode": { + "action": "Action", + "apply-damage": "Damage", + "check": "Check", + "damage": "Damage", + "flee": "Fleeing", + "generic": "Roll", + "morale": "Morale", + "rest": "Rest", + "usage": "Usage" + } + }, + "Weapon": { + "FIELDS": { + "description": { + "label": "Description" + }, + "category": { + "label": "Category" + }, + "damage": { + "label": "Damage" + }, + "range": { + "label": "Range" + }, + "properties": { + "label": "Properties" + }, + "usageDie": { + "label": "Usage Die" + }, + "quantity": { + "label": "Quantity" + }, + "equipped": { + "label": "Equipped" + }, + "broken": { + "label": "Broken" + } + } + }, + "Armor": { + "FIELDS": { + "description": { + "label": "Description" + }, + "armorDie": { + "label": "Armor Die" + }, + "penalty": { + "label": "Penalty" + }, + "equipped": { + "label": "Equipped" + }, + "broken": { + "label": "Broken" + } + } + }, + "Shield": { + "FIELDS": { + "description": { + "label": "Description" + }, + "armorDie": { + "label": "Armor Die" + }, + "penalty": { + "label": "Penalty" + }, + "equipped": { + "label": "Equipped" + }, + "broken": { + "label": "Broken" + } + } + }, + "Equipment": { + "FIELDS": { + "description": { + "label": "Description" + }, + "subtype": { + "label": "Subtype" + }, + "quantity": { + "label": "Quantity" + }, + "carried": { + "label": "Carried" + }, + "equipped": { + "label": "Equipped" + }, + "usageDie": { + "label": "Usage Die" + }, + "consumable": { + "label": "Consumable" + } + } + }, + "ResonanceCore": { + "FIELDS": { + "description": { + "label": "Description" + }, + "resonationId": { + "label": "Resonation" + }, + "usageDie": { + "label": "Usage Die" + }, + "burnedOut": { + "label": "Burned Out" + } + } + }, + "Artifact": { + "FIELDS": { + "description": { + "label": "Description" + }, + "artifactId": { + "label": "Artifact Id" + }, + "usageDie": { + "label": "Usage Die" + }, + "synchronized": { + "label": "Synchronized" + }, + "broken": { + "label": "Broken" + }, + "synchronizedTo": { + "label": "Synchronized To" + } + } + }, + "Feature": { + "FIELDS": { + "description": { + "label": "Description" + }, + "featureId": { + "label": "Feature Id" + } + } + } + }, + "TYPES": { + "Actor": { + "character": "Character", + "creature": "Creature", + "companion": "Companion" + }, + "Item": { + "weapon": "Weapon", + "armor": "Armor", + "shield": "Shield", + "equipment": "Equipment", + "resonance-core": "Resonance Core", + "artifact": "Artifact", + "feature": "Feature" + } + } +} diff --git a/less/base.less b/less/base.less new file mode 100644 index 0000000..78002de --- /dev/null +++ b/less/base.less @@ -0,0 +1,220 @@ +.application.mgne, +.mgne-chat-card { + --mgne-bg: @bg-char; + --mgne-panel: @bg-panel; + --mgne-panel-soft: @bg-panel-soft; + --mgne-border: fade(@bone, 35%); + --mgne-border-strong: fade(@gold-acid, 42%); + --mgne-accent: @ember-bright; + --mgne-accent-alt: @verdigris; + --mgne-text: @parchment; + --mgne-text-soft: @dust; + --mgne-title: @bone; + --mgne-shadow: @shadow-heavy; +} + +.application.mgne { + .body-copy(); + color: @parchment; + + .window-header { + background: + linear-gradient(90deg, fade(@blood, 40%), fade(@ember, 18%) 25%, transparent 70%), + linear-gradient(180deg, color-mix(in srgb, @bg-char 80%, black), @bg-void); + border-bottom: 1px solid fade(@gold-acid, 35%); + color: @bone; + + .window-title { + .caps-heading(); + font-size: 1rem; + text-shadow: 0 0 16px fade(@ember-bright, 25%); + } + + .header-control { + font-family: var(--font-awesome, "Font Awesome 6 Pro"); + font-style: normal; + font-variant: normal; + text-rendering: auto; + } + + .header-control::before, + i[class^="fa"], + i[class*=" fa"] { + font-family: inherit; + font-style: normal; + font-variant: normal; + text-rendering: auto; + } + + .header-control.fa-solid, + .header-control.fas, + .fa-solid, + .fas { + font-weight: 900; + } + + .header-control.fa-regular, + .header-control.far, + .fa-regular, + .far { + font-weight: 400; + } + } + + .window-content { + background: + radial-gradient(circle at top left, fade(@ember, 12%), transparent 28%), + radial-gradient(circle at top right, fade(@verdigris, 10%), transparent 24%), + linear-gradient(180deg, fade(@bg-char, 30%), fade(@bg-void, 28%)), + url("@{page-bg-url}") center top / cover no-repeat, + linear-gradient(180deg, @bg-char, @bg-void 120%); + color: @parchment; + min-height: 0; + overflow-y: auto; + scrollbar-gutter: stable; + } +} + +.application.mgne.character { + .window-content { + display: flex; + flex-direction: column; + overflow: hidden; + } +} + +.application.mgne *, +.mgne-chat-card * { + box-sizing: border-box; +} + +.application.mgne input, +.application.mgne select, +.application.mgne textarea, +.application.mgne button, +.application.mgne label, +.application.mgne p, +.application.mgne span, +.application.mgne small, +.mgne-chat-card { + .body-copy(); +} + +.application.mgne input, +.application.mgne select, +.application.mgne textarea { + width: 100%; + border: 1px solid fade(@bone, 22%); + border-radius: @radius-sm; + background: + linear-gradient(180deg, fade(@bone, 4%), transparent 60%), + @bg-input; + color: @parchment; + padding: 0.4rem 0.52rem; + box-shadow: inset 0 1px 0 fade(white, 4%); + + &:focus { + outline: none; + border-color: fade(@ember-bright, 70%); + box-shadow: 0 0 0 1px fade(@ember-bright, 25%), inset 0 1px 0 fade(white, 6%); + } +} + +.application.mgne select { + appearance: none; + padding-right: 1.3rem; + background-image: + linear-gradient(45deg, transparent 50%, fade(@gold-acid, 78%) 50%), + linear-gradient(135deg, fade(@gold-acid, 78%) 50%, transparent 50%), + linear-gradient(180deg, fade(@bone, 4%), transparent 60%); + background-position: + calc(100% - 11px) calc(50% - 2px), + calc(100% - 7px) calc(50% - 2px), + 0 0; + background-size: 4px 4px, 4px 4px, auto; + background-repeat: no-repeat; +} + +.application.mgne textarea { + min-height: 4.5rem; + resize: vertical; +} + +.application.mgne button { + .metal-button(); + padding: 0.34rem 0.72rem; +} + +.application.mgne label { + .caps-heading(); + color: @label-ink; + font-size: 0.74rem; +} + +.application.mgne fieldset { + .panel-shell(); + .ornate-frame(); + margin: 0; +} + +.application.mgne legend { + .caps-heading(); + color: @gold-acid; + display: inline-block; + position: relative; + top: 0.12rem; + font-size: 0.82rem; + line-height: 1.1; + padding: 0.04rem 0.32rem; +} + +.application.mgne code, +.mgne-chat-card code { + font-family: "Courier New", monospace; + background: fade(@bg-void, 75%); + color: @bone; + padding: 0.12rem 0.35rem; + border-radius: 4px; +} + +.application.mgne .empty-state { + color: fade(@ash, 94%); + font-style: italic; + letter-spacing: 0.03em; +} + +.application.mgne .rollable { + color: @ember-bright; + text-shadow: 0 0 12px fade(@ember, 18%); + + &:hover { + color: lighten(@ember-bright, 10%); + } +} + +#combat .combat-controls .mgne-flee-control { + display: flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + width: 100%; + min-height: 2rem; + margin-top: 0.35rem; + border: 1px solid fade(@gold-acid, 28%); + border-radius: @radius-sm; + background: + linear-gradient(180deg, fade(@blood, 26%), fade(@ember, 14%)), + fade(@bg-input, 92%); + color: @bone; + font-family: @font-body; + font-weight: 700; + + &:hover:not(:disabled) { + border-color: fade(@ember-bright, 52%); + color: lighten(@bone, 6%); + } + + &:disabled { + opacity: 0.5; + } +} diff --git a/less/chat.less b/less/chat.less new file mode 100644 index 0000000..5b47304 --- /dev/null +++ b/less/chat.less @@ -0,0 +1,215 @@ +.mgne-chat-card { + .panel-shell(); + .ornate-frame(); + display: flex; + flex-direction: column; + gap: 0.6rem; + padding: 0.68rem; + color: @iron; + background: + radial-gradient(circle at top right, fade(@verdigris, 10%), transparent 22%), + linear-gradient(135deg, fade(@ember, 10%), transparent 38%), + linear-gradient(180deg, fade(@bone, 4%), transparent 24%), + linear-gradient(180deg, fade(@bg-char, 22%), fade(@bg-void, 18%)), + url("@{page-bg-url}") right bottom / cover no-repeat, + @bg-panel; +} + +.mgne-chat-card .chat-card-header { + display: flex; + gap: 0.6rem; + align-items: center; +} + +.mgne-chat-card .chat-card-header img { + width: 36px; + height: 36px; + object-fit: cover; + border-radius: 50%; + border: 1px solid fade(@gold-acid, 38%); + box-shadow: 0 0 0 2px fade(@parchment, 6%); +} + +.mgne-chat-card .chat-eyebrow { + display: none; +} + +.mgne-chat-card h3 { + .caps-heading(); + margin: 0; + color: @label-ink; + font-size: 0.82rem; +} + +.mgne-chat-card .chat-actor, +.mgne-chat-card .chat-subtitle, +.mgne-chat-card .chat-formula, +.mgne-chat-card .chat-special { + margin: 0; +} + +.mgne-chat-card .chat-actor { + color: @iron; +} + +.mgne-chat-card .chat-formula { + color: @iron; + + code { + color: @parchment; + background: fade(@bg-void, 75%); + padding: 0.1em 0.42em; + border-radius: 3px; + } +} + +.mgne-chat-card .chat-result-line { + display: flex; + justify-content: space-between; + align-items: end; + padding: 0.38rem 0.55rem; + border: 1px solid fade(@gold-acid, 28%); + border-radius: @radius-sm; + background: fade(@bg-void, 78%); +} + +.mgne-chat-card .chat-result-label { + .caps-heading(); + font-size: 0.62rem; + color: @parchment; +} + +.mgne-chat-card .chat-result-total { + font-family: @font-display; + font-size: 1.08rem; + color: @parchment; +} + +.mgne-chat-card .chat-outcome { + .caps-heading(); + margin: 0; + padding: 0.3rem 0.55rem; + border-radius: @radius-sm; + background: fade(@bg-void, 72%); + border-left: 3px solid fade(@ember-bright, 70%); + color: lighten(@ember-bright, 12%); + font-size: 0.8rem; +} + +.mgne-chat-card .chat-special { + padding: 0.42rem 0.55rem; + border-left: 3px solid fade(@ember-bright, 55%); + background: fade(@bg-input, 85%); + color: @parchment; +} + +.mgne-chat-card.outcome-critical-success, +.mgne-chat-card.outcome-success, +.mgne-chat-card.outcome-steady { + border-color: fade(@verdigris, 42%); + + .chat-outcome { + color: lighten(@verdigris, 28%); + border-left-color: fade(@verdigris, 80%); + } +} + +.mgne-chat-card.outcome-failure, +.mgne-chat-card.outcome-broken, +.mgne-chat-card.outcome-fumble { + border-color: fade(@blood, 48%); + + .chat-outcome { + color: lighten(@ember-bright, 18%); + border-left-color: fade(@ember-bright, 80%); + } +} + +.mgne-chat-card.mode-apply-damage .chat-result-total, +.mgne-chat-card.mode-damage .chat-result-total { + color: lighten(@ember-bright, 8%); +} + +.mgne-chat-card .chat-card-actions { + margin-top: 0.2rem; +} + +.mgne-roll-damage-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + width: 100%; + padding: 0.38rem 0.7rem; + border: 1px solid @ember-bright; + border-radius: @radius-sm; + background: @ember; + color: #f8ede0; + font-family: @font-display; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.06em; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; + + .dmg-formula { + color: #fff5e8; + font-size: 0.75rem; + font-weight: 700; + } + + &:hover { + background: lighten(@ember, 8%); + border-color: lighten(@ember-bright, 12%); + color: #fff; + } + + &.is-critical { + border: 1px solid @gold-acid; + background: darken(@gold-acid, 10%); + color: #fff5d0; + font-weight: 700; + + .dmg-formula { + color: #fff; + } + + &:hover { + background: @gold-acid; + color: #fff; + } + } +} + +.chat-apply-actions { + margin-top: 0.35rem; +} + +.mgne-apply-damage-select { + width: 100%; + padding: 0.35rem 0.6rem; + border: 1px solid @ember-bright; + border-radius: @radius-sm; + background: @bg-char; + color: @bone; + font-family: @font-body; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; + + option { + background: @bg-char; + color: @bone; + } + + &:hover { + border-color: lighten(@ember-bright, 12%); + background: rgba(183, 70, 31, 0.18); + } + + &:focus { + outline: none; + border-color: @ember-bright; + } +} diff --git a/less/dialogs.less b/less/dialogs.less new file mode 100644 index 0000000..fd680eb --- /dev/null +++ b/less/dialogs.less @@ -0,0 +1,57 @@ +.application.mgne.roll-dialog .window-content { + padding: 0.55rem; + background: + radial-gradient(circle at top left, fade(@ember-bright, 14%), transparent 26%), + linear-gradient(180deg, fade(@bg-char, 26%), fade(@bg-void, 22%)), + url("@{page-bg-url}") center center / cover no-repeat, + linear-gradient(180deg, @bg-char, @bg-void 120%); +} + +.application.mgne.roll-dialog .dialog-content { + padding: 0; +} + +.application.mgne.roll-dialog .mgne-roll-dialog { + background: + linear-gradient(135deg, fade(@ember, 10%), transparent 32%), + linear-gradient(180deg, fade(@bg-char, 94%), fade(@bg-void, 98%)), + @bg-void; + border: 1px solid fade(@gold-acid, 28%); + border-radius: @radius-sm; + box-shadow: @shadow-heavy, @shadow-inset; + .ornate-frame(); + display: flex; + flex-direction: column; + gap: 0.55rem; + padding: 0.7rem; + + > p:first-child { + .caps-heading(); + font-size: 0.9rem; + color: @gold-acid; + margin: 0; + } + + > p:nth-child(2) { + margin: 0; + color: @bone; + font-style: italic; + } + + label { + color: @parchment; + } + + strong { + color: @parchment; + } +} + +.application.mgne.roll-dialog .mgne-roll-dialog label, +.application.mgne.roll-dialog .mgne-roll-dialog .checkbox-line { + color: @parchment; +} + +.application.mgne.roll-dialog .form-footer button { + min-width: 132px; +} diff --git a/less/fonts.less b/less/fonts.less new file mode 100644 index 0000000..3b467e4 --- /dev/null +++ b/less/fonts.less @@ -0,0 +1,13 @@ +@font-face { + font-family: "CastorTwoMGNE"; + src: url("../assets/fonts/CASTOR TWO W01 REGULAR.TTF") format("truetype"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: "LoraMGNE"; + src: url("../assets/fonts/LORA-REGULAR.TTF") format("truetype"); + font-weight: 400; + font-style: normal; +} diff --git a/less/mgne.less b/less/mgne.less new file mode 100644 index 0000000..2b66f27 --- /dev/null +++ b/less/mgne.less @@ -0,0 +1,7 @@ +@import "fonts.less"; +@import "variables.less"; +@import "mixins.less"; +@import "base.less"; +@import "sheets.less"; +@import "dialogs.less"; +@import "chat.less"; diff --git a/less/mixins.less b/less/mixins.less new file mode 100644 index 0000000..7a49f58 --- /dev/null +++ b/less/mixins.less @@ -0,0 +1,57 @@ +.panel-shell() { + position: relative; + background: + linear-gradient(180deg, fade(@bone, 4%), transparent 24%), + linear-gradient(135deg, fade(@ember, 8%), transparent 40%), + linear-gradient(180deg, fade(@bg-char, 52%), fade(@bg-void, 34%)), + url("@{page-bg-url}") center center / cover no-repeat, + linear-gradient(180deg, @bg-panel 0%, color-mix(in srgb, @bg-panel 88%, black) 100%); + border: @border-main; + border-radius: @radius-md; + box-shadow: @shadow-heavy, @shadow-inset; +} + +.ornate-frame() { + position: relative; + + &::before { + content: ""; + position: absolute; + inset: 6px; + pointer-events: none; + border: 1px solid fade(@gold-acid, 16%); + border-radius: calc(@radius-md - 4px); + } +} + +.caps-heading() { + font-family: @font-display; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.body-copy() { + font-family: @font-body; + line-height: 1.45; +} + +.metal-button() { + .caps-heading(); + border: 1px solid fade(@gold-acid, 44%); + border-radius: 999px; + background: + linear-gradient(180deg, fade(@ember-bright, 20%), fade(@blood, 10%)), + linear-gradient(135deg, fade(@bone, 10%), transparent 40%), + @bg-input; + color: @parchment; + box-shadow: inset 0 1px 0 fade(white, 8%), 0 6px 14px fade(black, 20%); + transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease, color 120ms ease; + + &:hover, + &:focus { + border-color: fade(@ember-bright, 70%); + color: lighten(@parchment, 8%); + transform: translateY(-1px); + box-shadow: inset 0 1px 0 fade(white, 14%), 0 8px 18px fade(@blood, 26%); + } +} diff --git a/less/sheets.less b/less/sheets.less new file mode 100644 index 0000000..e3ec222 --- /dev/null +++ b/less/sheets.less @@ -0,0 +1,643 @@ +.application.mgne .mgne-sheet { + display: flex; + flex-direction: column; + gap: 0.65rem; +} + +.application.mgne.character .mgne-sheet-header { + flex: 0 0 auto; +} + +.application.mgne .sheet-header { + display: grid; + grid-template-columns: 148px 1fr; + gap: 0.8rem; + align-items: start; +} + +.application.mgne.item-sheet .sheet-header { + grid-template-columns: 111px 1fr; +} + +.application.mgne .actor-portrait, +.application.mgne .item-portrait { + width: 100%; + max-width: 148px; + aspect-ratio: 1 / 1; + object-fit: cover; + border-radius: @radius-lg; + border: 1px solid fade(@gold-acid, 40%); + box-shadow: + 0 12px 24px fade(black, 34%), + inset 0 0 0 2px fade(@parchment, 8%); + background: + radial-gradient(circle at 30% 20%, fade(@bone, 8%), transparent 30%), + @bg-input; +} + +.application.mgne.item-sheet .item-portrait { + max-width: 111px; +} + +.application.mgne .header-fields, +.application.mgne .resource-bar, +.application.mgne .ability-grid, +.application.mgne .inventory-section, +.application.mgne .daily-resources-panel, +.application.mgne .tab-panel { + .panel-shell(); + .ornate-frame(); + padding: 0.65rem; +} + +.application.mgne .resource-bar, +.application.mgne .ability-grid, +.application.mgne .grid.two, +.application.mgne .grid.three, +.application.mgne .condition-value-grid, +.application.mgne .condition-flag-grid { + display: grid; + gap: 0.5rem; +} + +.application.mgne .resource-bar { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.application.mgne .resource-bar-core { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.application.mgne .resource-bar-daily { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.application.mgne .resource-bar-equipment { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.application.mgne .ability-grid { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.application.mgne .grid.two { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.application.mgne .grid.three { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.application.mgne.item-sheet .item-form-grid { + display: grid; + gap: 0.65rem 0.9rem; + grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); +} + +.application.mgne.item-sheet .item-form-grid-two { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.application.mgne.item-sheet .item-form-grid-three { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.application.mgne.item-sheet .item-form-row { + display: grid; + grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr); + align-items: center; + gap: 0.55rem; + min-width: 0; + + > label { + margin: 0; + line-height: 1.1; + min-width: 0; + } + + > input, + > select { + width: 100%; + min-width: 0; + } +} + +.application.mgne .condition-value-grid { + grid-template-columns: max-content 1fr; + align-items: center; + margin-bottom: 0.6rem; +} + +.application.mgne .condition-flag-grid { + grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); + gap: 0.3rem 0.6rem; +} + +.application.mgne .resource-box, +.application.mgne .ability-card { + position: relative; + display: flex; + flex-direction: column; + gap: 0.28rem; + padding: 0.52rem; + border-radius: @radius-md; + border: 1px solid fade(@iron, 30%); + background: + linear-gradient(180deg, fade(@bone, 4%), transparent 30%), + linear-gradient(180deg, fade(@ember, 7%), transparent 80%), + @bg-panel-soft; +} + +.application.mgne .ability-card { + min-height: 0; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 0.45rem; + padding: 0.38rem 0.46rem; +} + +.application.mgne .resource-box-track, +.application.mgne .resource-box-single { + justify-content: space-between; +} + +.application.mgne .resource-box-compact { + gap: 0.18rem; + padding: 0.36rem 0.42rem; + + > label { + font-size: 0.68rem; + letter-spacing: 0.11em; + } +} + +.application.mgne .resource-box-inline { + flex-direction: row; + align-items: center; + gap: 0.5rem; + + > label { + flex: 0 0 auto; + margin: 0; + } +} + +.application.mgne .resource-box-inline .resource-track, +.application.mgne .resource-box-inline .numeric-pill { + flex: 1 1 auto; +} + +.application.mgne .resource-box-inline-track { + align-items: flex-start; + + > label { + padding-top: 1rem; + } +} + +.application.mgne .resource-box-inline-track .numeric-caption { + color: @label-ink; +} + +.application.mgne .resource-box-inline-track .numeric-caption-strong { + padding: 0.08rem 0.34rem; + border-radius: 999px; + background: fade(@gold-acid, 18%); + border: 1px solid fade(@label-ink, 28%); + color: darken(@label-ink, 4%); + font-size: 0.6rem; + letter-spacing: 0.11em; +} + +.application.mgne .resource-box-inline-track .numeric-cluster { + align-items: center; +} + +.application.mgne .resource-box-inline-track .numeric-input { + width: 3.4rem; + min-width: 3.4rem; +} + +.application.mgne .resource-box-inline-single .numeric-pill { + flex: 0 0 auto; +} + +.application.mgne .resource-box-inline-single .numeric-input { + width: 3.2rem; + min-width: 3.2rem; +} + +.application.mgne .header-resource { + align-self: stretch; +} + +.application.mgne .header-resource .resource-box { + height: 100%; +} + +.application.mgne .resource-track { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 0.32rem; + align-items: end; +} + +.application.mgne .resource-track-die { + grid-template-columns: 1fr 1fr; +} + +.application.mgne .numeric-cluster { + display: flex; + flex-direction: column; + gap: 0.18rem; +} + +.application.mgne .numeric-caption { + .caps-heading(); + color: fade(@label-soft, 94%); + font-size: 0.58rem; + letter-spacing: 0.12em; +} + +.application.mgne .resource-box-compact .numeric-caption { + font-size: 0.52rem; + letter-spacing: 0.09em; +} + +.application.mgne .track-separator { + align-self: center; + color: fade(@gold-acid, 72%); + font-family: @font-display; + font-size: 1rem; + line-height: 1; +} + +.application.mgne .numeric-input { + min-width: 0; + text-align: center; + text-align-last: center; + font-variant-numeric: tabular-nums; + font-weight: 700; + font-size: 1rem; + padding-inline: 0.2rem; +} + +.application.mgne .resource-box-compact .numeric-input, +.application.mgne .resource-box-compact .compact-select { + min-height: 30px; + padding: 0.18rem 0.16rem; + font-size: 0.9rem; +} + +.application.mgne .numeric-input-readonly { + color: @bone; +} + +.application.mgne .compact-select { + text-align: center; + text-align-last: center; + font-variant-numeric: tabular-nums; + font-weight: 700; + padding-inline: 0.35rem; +} + +.application.mgne .numeric-pill { + display: flex; + align-items: center; + justify-content: center; + padding: 0.18rem; + border-radius: @radius-md; + border: 1px solid fade(@gold-acid, 18%); + background: fade(@bg-input, 64%); +} + +.application.mgne .resource-box-compact .numeric-pill { + padding: 0.1rem; +} + +.application.mgne .numeric-pill-suffix { + gap: 0.35rem; + justify-content: space-between; +} + +.application.mgne .numeric-suffix { + flex: 0 0 auto; + color: @gold-acid; + font-weight: 700; + font-size: 1rem; + line-height: 1; +} + +.application.mgne .resource-meta { + display: inline-flex; + align-self: flex-start; + margin-top: 0.12rem; + padding: 0.16rem 0.44rem; + border-radius: 999px; + border: 1px solid fade(@verdigris, 20%); + color: lighten(@verdigris, 18%); + background: fade(@verdigris, 10%); + font-size: 0.72rem; +} + +.application.mgne .resource-box-compact .resource-track { + gap: 0.22rem; +} + +.application.mgne .resource-box-compact .numeric-cluster { + gap: 0.1rem; +} + +.application.mgne .resource-box-compact .track-separator { + font-size: 0.88rem; +} + +.application.mgne .resource-box-compact .resource-meta { + margin-top: 0.06rem; + padding: 0.1rem 0.34rem; + font-size: 0.64rem; +} + +.application.mgne .resource-inline { + display: flex; + gap: 0.32rem; + align-items: center; + + span { + color: @dust; + } +} + +.application.mgne .sheet-tabs { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + flex: 0 0 auto; + justify-content: center; +} + +.application.mgne .tab-button { + min-width: 110px; +} + +.application.mgne .tab-button.active { + border-color: fade(@ember-bright, 70%); + background: + linear-gradient(180deg, fade(@ember-bright, 26%), fade(@blood, 12%)), + linear-gradient(135deg, fade(@parchment, 12%), transparent 45%), + @bg-input; + color: lighten(@parchment, 6%); +} + +.application.mgne .tab-panel:not(.active) { + display: none; +} + +.application.mgne.character .tab-panel.active { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; +} + +.application.mgne .inventory-section { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.application.mgne .daily-resources-panel { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.application.mgne .section-heading { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 0.5rem; + flex-wrap: wrap; +} + +.application.mgne .section-heading h2 { + .caps-heading(); + margin: 0; + color: @bone; + font-size: 0.9rem; +} + +.application.mgne .section-heading small { + display: inline-block; + margin-top: 0.12rem; + color: @dust; + font-size: 0.72rem; +} + +.application.mgne .inventory-header, +.application.mgne .inline-buttons, +.application.mgne .item-actions, +.application.mgne .sheet-actions { + display: flex; + gap: 0.35rem; + align-items: center; + flex-wrap: wrap; +} + +.application.mgne .resource-box-actions { + display: flex; + justify-content: flex-start; + margin-top: 0.12rem; +} + +.application.mgne .resource-box-actions-rest { + gap: 0.32rem; + flex-wrap: wrap; + + > button { + flex: 1 1 0; + min-width: 0; + padding-inline: 0.5rem; + white-space: nowrap; + } +} + +.application.mgne .inventory-header { + justify-content: space-between; +} + +.application.mgne .inventory-header h3 { + .caps-heading(); + margin: 0; + color: @bone; + font-size: 0.82rem; +} + +.application.mgne .item-row { + position: relative; + display: grid; + grid-template-columns: 2fr 1fr 1fr auto; + gap: 0.45rem; + align-items: center; + padding: 0.48rem 0.2rem 0.48rem 0.55rem; + border-radius: @radius-sm; + border: 1px solid transparent; + background: + linear-gradient(90deg, fade(@ember, 9%), transparent 35%), + linear-gradient(180deg, fade(@parchment, 2%), fade(@bg-void, 12%)), + fade(@bg-input, 80%); + + &:hover { + border-color: fade(@gold-acid, 28%); + background: + linear-gradient(90deg, fade(@ember, 12%), transparent 35%), + linear-gradient(180deg, fade(@parchment, 4%), fade(@bg-void, 12%)), + fade(@bg-input, 88%); + } +} + +.application.mgne .item-name { + color: @parchment; + font-weight: 700; + letter-spacing: 0.02em; + font-size: 0.95rem; +} + +.application.mgne .check-grid { + display: flex; + gap: 0.7rem; + flex-wrap: wrap; +} + +.application.mgne.item-sheet .item-check-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); + gap: 0.45rem 0.9rem; + align-items: center; +} + +.application.mgne.item-sheet .check-grid > label { + display: inline-flex; + align-items: center; + gap: 0.35rem; + flex: 0 0 auto; + width: fit-content; + margin: 0; +} + +.application.mgne .checkbox-line { + display: inline-flex; + gap: 0.35rem; + align-items: center; + width: fit-content; + + &.active { + color: @ember-bright; + font-weight: 600; + } +} + +.application.mgne .check-grid input[type="checkbox"], +.application.mgne .checkbox-line input[type="checkbox"] { + --checkbox-size: 0.9rem; + flex: 0 0 0.9rem; + width: 0.9rem; + height: 0.9rem; + min-width: 0.9rem; + padding: 0; + margin: 0; + position: relative; +} + +@media (max-width: 820px) { + .application.mgne.item-sheet .item-form-grid-two, + .application.mgne.item-sheet .item-form-grid-three { + grid-template-columns: minmax(0, 1fr); + } +} + +.application.mgne .ability-label { + display: inline-flex; + flex: 1 1 auto; + align-items: center; + justify-content: flex-start; + min-width: 0; +} + +.application.mgne .resource-label-accent { + color: @ember-bright; + text-shadow: 0 0 12px fade(@ember, 18%); +} + +.application.mgne .ability-score { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 58px; + min-height: 32px; + padding: 0.08rem; + border-radius: @radius-sm; + border: 1px solid fade(@gold-acid, 14%); + background: fade(@bg-input, 54%); +} + +.application.mgne .ability-input { + font-family: @font-display; + font-size: 1.05rem; + line-height: 1; + color: @bone; + padding: 0.12rem; +} + +.application.mgne select.ability-input { + flex: 1 1 auto; + width: 100%; +} + +.application.mgne .ability-score-text { + flex-direction: column; + gap: 0.02rem; +} + +.application.mgne .ability-defense-main { + .caps-heading(); + color: @bone; + font-size: 0.8rem; +} + +.application.mgne .ability-defense-sub { + color: @dust; + font-size: 0.62rem; +} + +.application.mgne .character .ability-card:nth-child(odd), +.application.mgne .character .resource-box:nth-child(odd) { + background: + linear-gradient(180deg, fade(@verdigris, 10%), transparent 55%), + linear-gradient(180deg, fade(@parchment, 4%), transparent 35%), + @bg-panel-soft; +} + +.application.mgne .creature .resource-box, +.application.mgne .companion .resource-box, +.application.mgne .creature .ability-card, +.application.mgne .companion .ability-card { + background: + linear-gradient(180deg, fade(@blood, 11%), transparent 60%), + linear-gradient(180deg, fade(@parchment, 4%), transparent 35%), + @bg-panel-soft; +} + +@media (max-width: 960px) { + .application.mgne .sheet-header, + .application.mgne .resource-bar, + .application.mgne .ability-grid, + .application.mgne .grid.two, + .application.mgne .grid.three { + grid-template-columns: 1fr; + } + + .application.mgne .item-row { + grid-template-columns: 1fr; + } +} diff --git a/less/variables.less b/less/variables.less new file mode 100644 index 0000000..ed31934 --- /dev/null +++ b/less/variables.less @@ -0,0 +1,29 @@ +@font-display: "CastorTwoMGNE", "Palatino Linotype", serif; +@font-body: "LoraMGNE", "Book Antiqua", serif; +@page-bg-url: "../assets/ui/page_background.webp"; + +@bg-void: #13100f; +@bg-char: #1b1512; +@bg-panel: rgba(41, 30, 24, 0.88); +@bg-panel-soft: rgba(64, 47, 37, 0.66); +@bg-input: rgba(17, 12, 10, 0.72); +@parchment: #ccb292; +@bone: #ab8b68; +@dust: #7e664f; +@ash: #5f4d40; +@label-ink: #6e3d2a; +@label-soft: #81533b; +@iron: #52453c; +@copper: #8d5f3f; +@verdigris: #4f7d73; +@ember: #b7461f; +@ember-bright: #dd6b2d; +@blood: #7f1d17; +@gold-acid: #c49a45; +@shadow-heavy: 0 14px 30px rgba(0, 0, 0, 0.36); +@shadow-inset: inset 0 1px 0 rgba(255, 236, 203, 0.08), inset 0 0 0 1px rgba(255, 236, 203, 0.03); +@border-main: 1px solid fade(@bone, 34%); +@border-strong: 1px solid fade(@gold-acid, 45%); +@radius-sm: 6px; +@radius-md: 8px; +@radius-lg: 12px; diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs new file mode 100644 index 0000000..4d29649 --- /dev/null +++ b/module/applications/_module.mjs @@ -0,0 +1,12 @@ +export { default as MGNEActorSheet } from "./sheets/base-actor-sheet.mjs" +export { default as MGNEItemSheet } from "./sheets/base-item-sheet.mjs" +export { default as MGNECharacterSheet } from "./sheets/character-sheet.mjs" +export { default as MGNECreatureSheet } from "./sheets/creature-sheet.mjs" +export { default as MGNECompanionSheet } from "./sheets/companion-sheet.mjs" +export { default as MGNEWeaponSheet } from "./sheets/weapon-sheet.mjs" +export { default as MGNEArmorSheet } from "./sheets/armor-sheet.mjs" +export { default as MGNEShieldSheet } from "./sheets/shield-sheet.mjs" +export { default as MGNEEquipmentSheet } from "./sheets/equipment-sheet.mjs" +export { default as MGNEResonanceCoreSheet } from "./sheets/resonance-core-sheet.mjs" +export { default as MGNEArtifactSheet } from "./sheets/artifact-sheet.mjs" +export { default as MGNEFeatureSheet } from "./sheets/feature-sheet.mjs" diff --git a/module/applications/sheets/armor-sheet.mjs b/module/applications/sheets/armor-sheet.mjs new file mode 100644 index 0000000..511564d --- /dev/null +++ b/module/applications/sheets/armor-sheet.mjs @@ -0,0 +1,7 @@ +import MGNEItemSheet from "./base-item-sheet.mjs" + +export default class MGNEArmorSheet extends MGNEItemSheet { + static PARTS = { + main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/armor.hbs" }, + } +} diff --git a/module/applications/sheets/artifact-sheet.mjs b/module/applications/sheets/artifact-sheet.mjs new file mode 100644 index 0000000..adda3ec --- /dev/null +++ b/module/applications/sheets/artifact-sheet.mjs @@ -0,0 +1,7 @@ +import MGNEItemSheet from "./base-item-sheet.mjs" + +export default class MGNEArtifactSheet extends MGNEItemSheet { + static PARTS = { + main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/artifact.hbs" }, + } +} diff --git a/module/applications/sheets/base-actor-sheet.mjs b/module/applications/sheets/base-actor-sheet.mjs new file mode 100644 index 0000000..5376d1c --- /dev/null +++ b/module/applications/sheets/base-actor-sheet.mjs @@ -0,0 +1,156 @@ +import { SYSTEM } from "../../config/system.mjs" +import { buildSharedSelectOptions } from "./select-options.mjs" +import { enrichHTMLFields } from "./rich-text.mjs" + +const { HandlebarsApplicationMixin } = foundry.applications.api + +export default class MGNEActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) { + static DEFAULT_OPTIONS = { + classes: ["mgne", "actor-sheet"], + position: { + width: 900, + height: "auto", + }, + window: { + resizable: true, + }, + form: { + submitOnChange: true, + }, + actions: { + editImage: MGNEActorSheet.onEditImage, + changeTab: MGNEActorSheet.onChangeTab, + createItem: MGNEActorSheet.onCreateItem, + editItem: MGNEActorSheet.onEditItem, + deleteItem: MGNEActorSheet.onDeleteItem, + toggleEquipped: MGNEActorSheet.onToggleEquipped, + syncArtifact: MGNEActorSheet.onSyncArtifact, + resetDaily: MGNEActorSheet.onResetDaily, + rollResonancePerDay: MGNEActorSheet.onRollResonancePerDay, + quickRest: MGNEActorSheet.onQuickRest, + fullRest: MGNEActorSheet.onFullRest, + }, + } + + async _prepareContext() { + const systemFields = this.document.system.schema.fields + + return { + fields: this.document.schema.fields, + systemFields, + actor: this.document, + system: this.document.system, + source: this.document.toObject(), + config: SYSTEM, + enrichedFields: await enrichHTMLFields(this.document.system, systemFields), + isEditable: this.isEditable, + selectOptions: buildSharedSelectOptions(), + } + } + + _onRender(context, options) { + super._onRender?.(context, options) + this.element.querySelectorAll(".rollable").forEach(element => { + element.addEventListener("click", this._onRoll.bind(this)) + }) + } + + async _onRoll(event) { + const target = event.currentTarget + const itemId = target.closest("[data-item-id]")?.dataset.itemId + const rollType = target.dataset.rollType + + switch (rollType) { + case "ability": + return this.document.rollAbility(target.dataset.abilityId) + case "defense": + return this.document.rollDefense() + case "weapon": + return this.document.rollWeapon(itemId) + case "profile-attack": + return this.document.rollProfileAttack() + case "profile-damage": + return this.document.rollProfileDamage() + case "damage": + return this.document.rollDamage(itemId) + case "resonation": + return this.document.rollResonation(itemId) + case "morale": + return this.document.rollMorale() + case "usage": + return this.document.rollUsage(itemId) + default: + return null + } + } + + static async onEditImage(_event, target) { + const attr = target.dataset.edit + const current = foundry.utils.getProperty(this.document, attr) + const picker = new FilePicker({ + current, + type: "image", + callback: path => this.document.update({ [attr]: path }), + top: this.position.top + 40, + left: this.position.left + 10, + }) + return picker.browse() + } + + static onChangeTab(_event, target) { + const group = target.dataset.group + const tab = target.dataset.tab + this.tabGroups[group] = tab + this.render() + } + + static async onCreateItem(_event, target) { + const itemType = target.dataset.itemType + const typeLabel = SYSTEM.itemTypes[itemType]?.label ?? itemType + const [created] = await this.document.createEmbeddedDocuments("Item", [{ + name: game.i18n.format("MGNE.Common.NewItem", { type: typeLabel }), + type: itemType, + img: SYSTEM.itemTypes[itemType]?.icon, + }]) + created?.sheet?.render(true) + } + + static async onEditItem(_event, target) { + const itemId = target.closest("[data-item-id]")?.dataset.itemId + const item = this.document.items.get(itemId) + return item?.sheet?.render(true) + } + + static async onDeleteItem(_event, target) { + const itemId = target.closest("[data-item-id]")?.dataset.itemId + const item = this.document.items.get(itemId) + if (!item) return null + return item.delete() + } + + static async onToggleEquipped(_event, target) { + const itemId = target.closest("[data-item-id]")?.dataset.itemId + return this.document.toggleItemEquipped(itemId) + } + + static async onSyncArtifact(_event, target) { + const itemId = target.closest("[data-item-id]")?.dataset.itemId + return this.document.syncArtifact(itemId) + } + + static async onResetDaily() { + return this.document.resetDaily() + } + + static async onRollResonancePerDay() { + return this.document.rollResonancePerDay() + } + + static async onQuickRest() { + return this.document.quickRest() + } + + static async onFullRest() { + return this.document.fullRest() + } +} diff --git a/module/applications/sheets/base-item-sheet.mjs b/module/applications/sheets/base-item-sheet.mjs new file mode 100644 index 0000000..e9a4ab1 --- /dev/null +++ b/module/applications/sheets/base-item-sheet.mjs @@ -0,0 +1,56 @@ +import { SYSTEM } from "../../config/system.mjs" +import { buildSharedSelectOptions, numericOptions } from "./select-options.mjs" +import { enrichHTMLFields } from "./rich-text.mjs" + +const { HandlebarsApplicationMixin } = foundry.applications.api + +export default class MGNEItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) { + static DEFAULT_OPTIONS = { + classes: ["mgne", "item-sheet"], + position: { + width: 720, + height: "auto", + }, + window: { + resizable: true, + }, + form: { + submitOnChange: true, + }, + actions: { + editImage: MGNEItemSheet.onEditImage, + }, + } + + async _prepareContext() { + const selectOptions = buildSharedSelectOptions() + const systemFields = this.document.system.schema.fields + if (this.document.type === "armor") selectOptions.penalties = numericOptions(0, 6, this.document.system.penalty) + if (this.document.type === "shield") selectOptions.penalties = numericOptions(0, 4, this.document.system.penalty) + + return { + fields: this.document.schema.fields, + systemFields, + item: this.document, + system: this.document.system, + source: this.document.toObject(), + config: SYSTEM, + enrichedFields: await enrichHTMLFields(this.document.system, systemFields), + isEditable: this.isEditable, + selectOptions, + } + } + + static async onEditImage(_event, target) { + const attr = target.dataset.edit + const current = foundry.utils.getProperty(this.document, attr) + const picker = new FilePicker({ + current, + type: "image", + callback: path => this.document.update({ [attr]: path }), + top: this.position.top + 40, + left: this.position.left + 10, + }) + return picker.browse() + } +} diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs new file mode 100644 index 0000000..67cac91 --- /dev/null +++ b/module/applications/sheets/character-sheet.mjs @@ -0,0 +1,99 @@ +import MGNEActorSheet from "./base-actor-sheet.mjs" +import { SYSTEM } from "../../config/system.mjs" +import { buildCharacterSelectOptions } from "./select-options.mjs" + +export default class MGNECharacterSheet extends MGNEActorSheet { + static DEFAULT_OPTIONS = { + classes: ["character"], + position: { + width: 1040, + height: 760, + }, + } + + static PARTS = { + main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-main.hbs" }, + tabs: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-tabs.hbs" }, + overview: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-overview.hbs" }, + daily: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-daily.hbs" }, + equipment: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-equipment.hbs" }, + features: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-features.hbs" }, + notes: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-notes.hbs" }, + } + + tabGroups = { sheet: "overview" } + + getTabs() { + const tabs = { + overview: { id: "overview", group: "sheet", label: game.i18n.localize("MGNE.Tabs.overview") }, + daily: { id: "daily", group: "sheet", label: game.i18n.localize("MGNE.Tabs.daily") }, + equipment: { id: "equipment", group: "sheet", label: game.i18n.localize("MGNE.Tabs.equipment") }, + features: { id: "features", group: "sheet", label: game.i18n.localize("MGNE.Tabs.features") }, + notes: { id: "notes", group: "sheet", label: game.i18n.localize("MGNE.Tabs.notes") }, + } + for (const tab of Object.values(tabs)) { + tab.active = this.tabGroups[tab.group] === tab.id + tab.cssClass = tab.active ? "active" : "" + } + return tabs + } + + async _prepareContext() { + const context = await super._prepareContext() + context.tabs = this.getTabs() + context.abilityList = SYSTEM.abilityOrder.map(id => ({ + id, + ...SYSTEM.abilities[id], + value: context.source.system.abilities?.[id]?.value ?? 0, + })) + context.selectOptions = { + ...context.selectOptions, + ...buildCharacterSelectOptions(context.system), + } + return context + } + + async _preparePartContext(partId, context) { + const doc = this.document + switch (partId) { + case "overview": + context.tab = context.tabs.overview + context.valueConditions = Object.entries(doc.system.conditions ?? {}) + .filter(([id]) => SYSTEM.conditions[id]?.hasValue) + .map(([id, cond]) => ({ + id, + label: SYSTEM.conditions[id].label, + value: cond.value, + options: context.selectOptions.conditionValues, + })) + context.flagConditions = Object.entries(doc.system.conditions ?? {}) + .filter(([id]) => !SYSTEM.conditions[id]?.hasValue) + .map(([id, cond]) => ({ + id, + label: SYSTEM.conditions[id].label, + active: cond.active, + })) + break + case "daily": + context.tab = context.tabs.daily + break + case "equipment": + context.tab = context.tabs.equipment + context.weapons = doc.itemTypes.weapon + context.armors = doc.itemTypes.armor + context.shields = doc.itemTypes.shield + context.equipmentItems = doc.itemTypes.equipment + context.cores = doc.itemTypes["resonance-core"] + context.artifacts = doc.itemTypes.artifact + break + case "features": + context.tab = context.tabs.features + context.features = doc.itemTypes.feature + break + case "notes": + context.tab = context.tabs.notes + break + } + return context + } +} diff --git a/module/applications/sheets/companion-sheet.mjs b/module/applications/sheets/companion-sheet.mjs new file mode 100644 index 0000000..06bc4b5 --- /dev/null +++ b/module/applications/sheets/companion-sheet.mjs @@ -0,0 +1,26 @@ +import MGNEActorSheet from "./base-actor-sheet.mjs" +import { SYSTEM } from "../../config/system.mjs" + +export default class MGNECompanionSheet extends MGNEActorSheet { + static DEFAULT_OPTIONS = { + classes: ["companion"], + position: { + width: 820, + height: 700, + }, + } + + static PARTS = { + main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/companion-main.hbs" }, + } + + async _prepareContext() { + const context = await super._prepareContext() + context.abilityList = SYSTEM.abilityOrder.map(id => ({ + id, + ...SYSTEM.abilities[id], + value: context.source.system.abilities?.[id]?.value ?? 0, + })) + return context + } +} diff --git a/module/applications/sheets/creature-sheet.mjs b/module/applications/sheets/creature-sheet.mjs new file mode 100644 index 0000000..5f48f4c --- /dev/null +++ b/module/applications/sheets/creature-sheet.mjs @@ -0,0 +1,26 @@ +import MGNEActorSheet from "./base-actor-sheet.mjs" +import { SYSTEM } from "../../config/system.mjs" + +export default class MGNECreatureSheet extends MGNEActorSheet { + static DEFAULT_OPTIONS = { + classes: ["creature"], + position: { + width: 760, + height: 640, + }, + } + + static PARTS = { + main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/creature-main.hbs" }, + } + + async _prepareContext() { + const context = await super._prepareContext() + context.abilityList = SYSTEM.abilityOrder.map(id => ({ + id, + ...SYSTEM.abilities[id], + value: context.source.system.abilities?.[id]?.value ?? 0, + })) + return context + } +} diff --git a/module/applications/sheets/equipment-sheet.mjs b/module/applications/sheets/equipment-sheet.mjs new file mode 100644 index 0000000..4a75ba3 --- /dev/null +++ b/module/applications/sheets/equipment-sheet.mjs @@ -0,0 +1,7 @@ +import MGNEItemSheet from "./base-item-sheet.mjs" + +export default class MGNEEquipmentSheet extends MGNEItemSheet { + static PARTS = { + main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/equipment.hbs" }, + } +} diff --git a/module/applications/sheets/feature-sheet.mjs b/module/applications/sheets/feature-sheet.mjs new file mode 100644 index 0000000..1abf20b --- /dev/null +++ b/module/applications/sheets/feature-sheet.mjs @@ -0,0 +1,7 @@ +import MGNEItemSheet from "./base-item-sheet.mjs" + +export default class MGNEFeatureSheet extends MGNEItemSheet { + static PARTS = { + main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/feature.hbs" }, + } +} diff --git a/module/applications/sheets/resonance-core-sheet.mjs b/module/applications/sheets/resonance-core-sheet.mjs new file mode 100644 index 0000000..435a344 --- /dev/null +++ b/module/applications/sheets/resonance-core-sheet.mjs @@ -0,0 +1,7 @@ +import MGNEItemSheet from "./base-item-sheet.mjs" + +export default class MGNEResonanceCoreSheet extends MGNEItemSheet { + static PARTS = { + main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/resonance-core.hbs" }, + } +} diff --git a/module/applications/sheets/rich-text.mjs b/module/applications/sheets/rich-text.mjs new file mode 100644 index 0000000..a8c5660 --- /dev/null +++ b/module/applications/sheets/rich-text.mjs @@ -0,0 +1,17 @@ +export async function enrichHTMLFields(data, schemaFields) { + const enrichedFields = {} + + for (const [key, field] of Object.entries(schemaFields ?? {})) { + if (field instanceof foundry.data.fields.HTMLField) { + enrichedFields[key] = await foundry.applications.ux.TextEditor.implementation.enrichHTML(data?.[key] ?? "", { async: true }) + continue + } + + if (field instanceof foundry.data.fields.SchemaField) { + const nested = await enrichHTMLFields(data?.[key], field.fields) + if (Object.keys(nested).length) enrichedFields[key] = nested + } + } + + return enrichedFields +} diff --git a/module/applications/sheets/select-options.mjs b/module/applications/sheets/select-options.mjs new file mode 100644 index 0000000..e7762b7 --- /dev/null +++ b/module/applications/sheets/select-options.mjs @@ -0,0 +1,50 @@ +import { SYSTEM } from "../../config/system.mjs" + +function normalizeMax(max, current) { + return Math.max(max ?? 0, Number.isFinite(current) ? current : 0) +} + +export function numericOptions(min, max, current = null) { + const resolvedMin = Math.min(min, Number.isFinite(current) ? current : min) + const resolvedMax = normalizeMax(max, current) + return Array.from({ length: Math.max(0, resolvedMax - resolvedMin) + 1 }, (_, index) => { + const value = resolvedMin + index + return { value, label: String(value) } + }) +} + +export function objectOptions(choices) { + return Object.entries(choices).map(([value, label]) => ({ value, label })) +} + +export function dieMax(die) { + if (typeof die !== "string" || !die.startsWith("d")) return 0 + const faces = Number.parseInt(die.slice(1), 10) + return Number.isFinite(faces) ? faces : 0 +} + +export function buildSharedSelectOptions() { + return { + abilityValues: numericOptions(-3, 6), + conditionValues: numericOptions(0, 12), + moraleValues: numericOptions(2, 12), + armorPenalties: numericOptions(0, 6), + shieldPenalties: numericOptions(0, 4), + weaponCategories: objectOptions(SYSTEM.weaponCategories), + usageDice: objectOptions(SYSTEM.usageDieChoices), + armorDice: objectOptions(SYSTEM.armorDieChoices), + omenDice: objectOptions(SYSTEM.omenDieChoices), + resonanceList: objectOptions(SYSTEM.resonanceList), + equipmentSubtypes: objectOptions(SYSTEM.equipmentSubtypes), + artifactIds: objectOptions(SYSTEM.artifactChoices), + featureIds: objectOptions(SYSTEM.featureChoices), + } +} + +export function buildCharacterSelectOptions(system) { + return { + omenCurrent: numericOptions(0, dieMax(system.omens?.die), system.omens?.current), + resonanceUsed: numericOptions(0, system.resonance?.max ?? 0, system.resonance?.used), + artifactSyncUsed: numericOptions(0, system.syncLimit ?? 0, system.artifactSync?.used), + } +} diff --git a/module/applications/sheets/shield-sheet.mjs b/module/applications/sheets/shield-sheet.mjs new file mode 100644 index 0000000..5de506c --- /dev/null +++ b/module/applications/sheets/shield-sheet.mjs @@ -0,0 +1,7 @@ +import MGNEItemSheet from "./base-item-sheet.mjs" + +export default class MGNEShieldSheet extends MGNEItemSheet { + static PARTS = { + main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/shield.hbs" }, + } +} diff --git a/module/applications/sheets/weapon-sheet.mjs b/module/applications/sheets/weapon-sheet.mjs new file mode 100644 index 0000000..c88fc8a --- /dev/null +++ b/module/applications/sheets/weapon-sheet.mjs @@ -0,0 +1,7 @@ +import MGNEItemSheet from "./base-item-sheet.mjs" + +export default class MGNEWeaponSheet extends MGNEItemSheet { + static PARTS = { + main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/weapon.hbs" }, + } +} diff --git a/module/config/system.mjs b/module/config/system.mjs new file mode 100644 index 0000000..07e8a59 --- /dev/null +++ b/module/config/system.mjs @@ -0,0 +1,269 @@ +export const SYSTEM_ID = "fvtt-machine-gods-noxian-expanse" +export const ASCII = ` +Machine Gods of the Noxian Expanse +` + +const dieChoiceLabels = values => Object.fromEntries(values.map(value => [value, value === "depleted" ? "Depleted" : value.toUpperCase()])) +const keyedNameChoices = (items, key) => Object.fromEntries(items.map(item => [item[key], item.name])) +const toIconSlug = name => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") +const itemIcon = slug => `systems/${SYSTEM_ID}/assets/icons/${slug}.svg` +const withItemIcons = items => items.map(item => ({ ...item, img: itemIcon(toIconSlug(item.name)) })) +let localized = false + +function localizeEntryLabels(entries, prefix) { + for (const [key, entry] of Object.entries(entries)) { + entry.label = game.i18n.localize(`${prefix}.${key}`) + } +} + +function localizeChoiceLabels(entries, prefix) { + for (const key of Object.keys(entries)) { + entries[key] = game.i18n.localize(`${prefix}.${key}`) + } +} + +export const SYSTEM = { + id: SYSTEM_ID, + actorTypes: { + character: { id: "character", label: "Character" }, + creature: { id: "creature", label: "Creature" }, + companion: { id: "companion", label: "Companion" }, + }, + itemTypes: { + weapon: { id: "weapon", label: "Weapon", icon: itemIcon("weapon") }, + armor: { id: "armor", label: "Armor", icon: itemIcon("armor") }, + shield: { id: "shield", label: "Shield", icon: itemIcon("shield") }, + equipment: { id: "equipment", label: "Equipment", icon: itemIcon("equipment") }, + "resonance-core": { id: "resonance-core", label: "Resonance Core", icon: itemIcon("resonance-core") }, + artifact: { id: "artifact", label: "Artifact", icon: itemIcon("artifact") }, + feature: { id: "feature", label: "Feature", icon: itemIcon("feature") }, + }, + abilities: { + agility: { id: "agility", label: "Agility" }, + presence: { id: "presence", label: "Presence" }, + strength: { id: "strength", label: "Strength" }, + toughness: { id: "toughness", label: "Toughness" }, + }, + abilityOrder: ["agility", "presence", "strength", "toughness"], + conditions: { + bleeding: { id: "bleeding", label: "Bleeding", hasValue: true }, + blinded: { id: "blinded", label: "Blinded" }, + burning: { id: "burning", label: "Burning" }, + fatigued: { id: "fatigued", label: "Fatigued", hasValue: true }, + infected: { id: "infected", label: "Infected" }, + poisoned: { id: "poisoned", label: "Poisoned" }, + prone: { id: "prone", label: "Prone" }, + restrained: { id: "restrained", label: "Restrained" }, + starved: { id: "starved", label: "Starved" }, + stunned: { id: "stunned", label: "Stunned", hasValue: true }, + }, + usageDice: ["d12", "d10", "d8", "d6", "d4", "depleted"], + usageDieChoices: dieChoiceLabels(["d12", "d10", "d8", "d6", "d4", "depleted"]), + armorDice: ["d12", "d10", "d8", "d6", "d4", "d2", "0"], + armorDieChoices: dieChoiceLabels(["d12", "d10", "d8", "d6", "d4", "d2", "0"]), + omenDice: ["d2", "d4", "d6", "d8"], + omenDieChoices: dieChoiceLabels(["d2", "d4", "d6", "d8"]), + weaponCategories: { + melee: "Melee", + ranged: "Ranged", + }, + resonanceList: { + accelerate: "Accelerate", + blast: "Blast", + "breathe-water": "Breathe Water", + cauterize: "Cauterize", + "create-illusion": "Create Illusion", + distract: "Distract", + "eagle-eye": "Eagle Eye", + "empower-weapon": "Empower Weapon", + fireball: "Fireball", + hover: "Hover", + "influence-mind": "Influence Mind", + "knit-flesh": "Knit Flesh", + "light-construct": "Light Construct", + mirage: "Mirage", + "negate-injury": "Negate Injury", + paralyze: "Paralyze", + shield: "Shield", + shock: "Shock", + shroud: "Shroud", + "summon-mist": "Summon Mist", + }, + starterWeapons: withItemIcons([ + { name: "Club", damage: "1d4", category: "melee", range: "Touch" }, + { name: "Dagger", damage: "1d4", category: "melee", range: "Touch" }, + { name: "Handaxe", damage: "1d6", category: "melee", range: "Touch" }, + { name: "Quarterstaff", damage: "1d4", category: "melee", range: "Touch" }, + { name: "Whip", damage: "1d4", category: "melee", range: "Near" }, + { name: "Shortbow", damage: "1d6", category: "ranged", range: "Near/Far" }, + { name: "Spear", damage: "1d6", category: "melee", range: "Near" }, + { name: "Longsword", damage: "1d8", category: "melee", range: "Touch" }, + { name: "Heavy Crossbow", damage: "1d10", category: "ranged", range: "Far" }, + { name: "Rapier", damage: "1d6", category: "melee", range: "Touch" }, + { name: "Halberd", damage: "1d10", category: "melee", range: "Near" }, + { name: "Maul", damage: "1d10", category: "melee", range: "Touch" }, + ]), + starterArmor: withItemIcons([ + { name: "Clothing (Average)", type: "armor", armorDie: "d2", penalty: 0 }, + { name: "Helm", type: "armor", armorDie: "d2", penalty: 0 }, + { name: "Medium Shield", type: "shield", armorDie: "d4", penalty: 0 }, + { name: "Gambeson", type: "armor", armorDie: "d4", penalty: 0 }, + { name: "Padded Leather", type: "armor", armorDie: "d4", penalty: 1 }, + { name: "Chain Shirt", type: "armor", armorDie: "d6", penalty: 1 }, + { name: "Half Plate", type: "armor", armorDie: "d8", penalty: 2 }, + { name: "Full Plate", type: "armor", armorDie: "d10", penalty: 2 }, + ]), + starterEquipment: withItemIcons([ + { name: "Rations", type: "equipment", subtype: "consumable", quantity: 1, usageDie: "d8", consumable: true }, + { name: "Waterskin", type: "equipment", subtype: "travel", quantity: 1, usageDie: "d8", consumable: true }, + { name: "Sack", type: "equipment", subtype: "container", quantity: 1, usageDie: "depleted", consumable: false }, + { name: "Backpack", type: "equipment", subtype: "container", quantity: 1, usageDie: "depleted", consumable: false }, + { name: "Rope", type: "equipment", subtype: "tool", quantity: 1, usageDie: "depleted", consumable: false }, + { name: "Medical Supplies", type: "equipment", subtype: "tool", quantity: 1, usageDie: "d6", consumable: true }, + ]), + equipmentSubtypes: { + gear: "Gear", + consumable: "Consumable", + travel: "Travel", + container: "Container", + tool: "Tool", + }, + starterCores: withItemIcons([ + { name: "Accelerate Core", type: "resonance-core", resonationId: "accelerate", usageDie: "d6" }, + { name: "Blast Core", type: "resonance-core", resonationId: "blast", usageDie: "d6" }, + { name: "Cauterize Core", type: "resonance-core", resonationId: "cauterize", usageDie: "d6" }, + { name: "Empower Weapon Core", type: "resonance-core", resonationId: "empower-weapon", usageDie: "d6" }, + ]), + sampleArtifacts: withItemIcons([ + { name: "Shiver Lens", type: "artifact", artifactId: "shiver-lens", usageDie: "d6", description: "A cracked lens that reveals hidden heat signatures." }, + { name: "Processional Halo", type: "artifact", artifactId: "processional-halo", usageDie: "d6", description: "A floating crown of light that amplifies solemn commands." }, + { name: "Null Forge Spike", type: "artifact", artifactId: "null-forge-spike", usageDie: "d4", description: "A ritual spike used to jam malfunctioning relics into silence." }, + { name: "Lux Relay Idol", type: "artifact", artifactId: "lux-relay-idol", usageDie: "d6", description: "A palm-sized relay that stores and redirects ambient luminance." }, + ]), + artifactChoices: keyedNameChoices([ + { name: "Shiver Lens", artifactId: "shiver-lens" }, + { name: "Processional Halo", artifactId: "processional-halo" }, + { name: "Null Forge Spike", artifactId: "null-forge-spike" }, + { name: "Lux Relay Idol", artifactId: "lux-relay-idol" }, + ], "artifactId"), + sampleFeatures: withItemIcons([ + { name: "Akimbo Hit Priest", type: "feature", featureId: "akimbo-hit-priest", description: "Once per round after hitting with an attack, suffer Fatigued (1) to attack again." }, + { name: "Ambivalent Slouch", type: "feature", featureId: "ambivalent-slouch", description: "Gain +1 carrying capacity and reduce armor or shield Agility penalties by 1." }, + { name: "Avaricious Gubbingrifter", type: "feature", featureId: "avaricious-gubbingrifter", description: "Gain three random resonance cores and -1 DR to invoking resonations." }, + { name: "Backbiter", type: "feature", featureId: "backbiter", description: "When you win initiative, first-round attacks bypass 2d6 armor." }, + { name: "Chart Reading Maniac", type: "feature", featureId: "chart-reading-maniac", description: "Gain an extra exploration turn per day and reroll one exploration or ruin table per day." }, + { name: "Liturgicantal Blesswell", type: "feature", featureId: "liturgicantal-blesswell", description: "Restore d4 HP d4 times per full rest and repair one usage die by a step once per day." }, + ]), + featureChoices: keyedNameChoices([ + { name: "Akimbo Hit Priest", featureId: "akimbo-hit-priest" }, + { name: "Ambivalent Slouch", featureId: "ambivalent-slouch" }, + { name: "Avaricious Gubbingrifter", featureId: "avaricious-gubbingrifter" }, + { name: "Backbiter", featureId: "backbiter" }, + { name: "Chart Reading Maniac", featureId: "chart-reading-maniac" }, + { name: "Liturgicantal Blesswell", featureId: "liturgicantal-blesswell" }, + ], "featureId"), + companions: { + noble: { + name: "Beguiled Noble", + hp: 6, + morale: 9, + armorDie: "d4", + attackLabel: "Blade", + attackDamage: "1d6", + }, + mercenary: { + name: "Dustland Mercenary", + hp: 8, + morale: 7, + armorDie: "d4", + attackLabel: "Cleaver", + attackDamage: "1d4+2", + }, + pickpocket: { + name: "Scrapling Pickpocket", + hp: 3, + morale: 4, + armorDie: "0", + attackLabel: "Folding Knife", + attackDamage: "1d2", + }, + cantor: { + name: "Silicon Cantor", + hp: 4, + morale: 6, + armorDie: "0", + attackLabel: "Book Thump", + attackDamage: "1", + }, + }, + tables: { + eucatastrophe: [ + "Recover d4 HP.", + "Invoke the resonation again immediately.", + "Synchronize one additional artifact for the day.", + "Gain -1 DR to invoking resonations for five minutes.", + "Increase daily resonation uses by 2 for the day.", + "Gain +1 carrying capacity for the day.", + "Gain -4 DR to your next check within five minutes.", + "Gain +1 max HP for the day.", + "Recover one omen.", + "Borrow a random nearby feature or increase a random ability by 1 until end of day." + ], + catastrophe: [ + "You are reduced to 0 HP.", + "The resonation targets the wrong creature.", + "A random nearby artifact explodes.", + "The resonation is cast twice at random targets.", + "You become Restrained for five minutes.", + "You are knocked Prone and suffer Stunned (2).", + "Your worn armor is consumed.", + "Suffer Fatigued (1), doubling if repeated the same day.", + "Discard one omen.", + "All artifacts desynchronize and you cannot invoke resonations for the day." + ], + triumphs: [ + "Discover a useful resource.", + "Gain -3 DR to your next check within five minutes.", + "Discard a condition or recover d2 HP.", + "Grant a nearby ally -2 DR on their next check.", + "Ignore all penalties on your next check.", + "Discard Prone and Restrained, then move immediately.", + "Restore a weapon or armor usage die by one step.", + "Take an immediate action or find 3d10 coins.", + "Take an immediate action or gain -2 DR to your next check.", + "Move one point from one ability to another for one hour." + ], + mishaps: [ + "Downgrade involved equipment usage dice or suffer d4 damage bypassing armor.", + "A hostile random creature investigates.", + "Start an uncontrolled fire or suffer d4 damage bypassing armor.", + "Suffer Infected.", + "Suffer +1 DR on checks until you fully rest.", + "You cannot invoke resonations for d8 hours.", + "Suffer +3 DR on your next check this hour.", + "Suffer d6 damage bypassing armor, minimum 1 HP remaining.", + "Suffer Bleeding (1) in combat or d4 damage bypassing armor outside combat.", + "Roll twice on the mishaps table and take both results." + ], + breakResults: { + 1: "Unconscious for d4 rounds, then wake with d4 HP.", + 2: "As above, and a random working limb becomes useless until treated.", + 3: "As above, and without emergency treatment you die in d2 hours.", + 4: "Immediate death." + } + } +} + +export function localizeSystemConfig() { + if (localized) return + localized = true + + localizeEntryLabels(SYSTEM.actorTypes, "MGNE.ActorTypes") + localizeEntryLabels(SYSTEM.itemTypes, "MGNE.ItemTypes") + localizeEntryLabels(SYSTEM.abilities, "MGNE.Abilities") + localizeEntryLabels(SYSTEM.conditions, "MGNE.Conditions") + localizeChoiceLabels(SYSTEM.weaponCategories, "MGNE.WeaponCategories") + localizeChoiceLabels(SYSTEM.resonanceList, "MGNE.Resonations") + localizeChoiceLabels(SYSTEM.equipmentSubtypes, "MGNE.EquipmentSubtypes") + SYSTEM.usageDieChoices.depleted = game.i18n.localize("MGNE.Common.Depleted") +} diff --git a/module/documents/_module.mjs b/module/documents/_module.mjs new file mode 100644 index 0000000..44d8f11 --- /dev/null +++ b/module/documents/_module.mjs @@ -0,0 +1,4 @@ +export { default as MGNEActor } from "./actor.mjs" +export { default as MGNECombat } from "./combat.mjs" +export { default as MGNEItem } from "./item.mjs" +export { default as MGNERoll } from "./roll.mjs" diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs new file mode 100644 index 0000000..ce4c16d --- /dev/null +++ b/module/documents/actor.mjs @@ -0,0 +1,386 @@ +import MGNERoll from "./roll.mjs" +import { SYSTEM, SYSTEM_ID } from "../config/system.mjs" + +const t = key => game.i18n.localize(key) +const f = (key, data = {}) => game.i18n.format(key, data) +const PENDING_DAMAGE_FLAG = "pendingDamageBonus" +const PENDING_DEFENSE_FLAG = "pendingDefenseFumble" + +function formatActionLabel(baseLabel, actionLabel) { + const trimmedBase = `${baseLabel ?? ""}`.trim() + if (!trimmedBase) return actionLabel + return trimmedBase.toLowerCase().endsWith(actionLabel.toLowerCase()) ? trimmedBase : `${trimmedBase} ${actionLabel}` +} + +function normalizeGenericActionLabel(baseLabel, genericLabel) { + const trimmedBase = `${baseLabel ?? ""}`.trim() + if (!trimmedBase) return genericLabel + + const normalizedWords = trimmedBase + .split(/\s+/) + .filter(Boolean) + .map(word => word.toLowerCase()) + + return normalizedWords.every(word => word === genericLabel.toLowerCase()) ? genericLabel : trimmedBase +} + +function maxDieByRank(items, property) { + const rank = { d12: 0, d10: 1, d8: 2, d6: 3, d4: 4, d2: 5, 0: 6, depleted: 7 } + return [...items].sort((left, right) => (rank[left.system[property]] ?? 99) - (rank[right.system[property]] ?? 99))[0] ?? null +} + +export default class MGNEActor extends Actor { + async setPendingDamageBonus(contextId, multiplier = 2) { + await this.setFlag(SYSTEM_ID, PENDING_DAMAGE_FLAG, { contextId, multiplier }) + } + + async consumePendingDamageBonus(contextId) { + const bonus = this.getFlag(SYSTEM_ID, PENDING_DAMAGE_FLAG) + if (!bonus || bonus.contextId !== contextId) return null + await this.unsetFlag(SYSTEM_ID, PENDING_DAMAGE_FLAG) + return bonus + } + + async setPendingDefenseFumble() { + await this.setFlag(SYSTEM_ID, PENDING_DEFENSE_FLAG, { active: true }) + } + + async consumePendingDefenseFumble() { + const pending = this.getFlag(SYSTEM_ID, PENDING_DEFENSE_FLAG) + if (!pending?.active) return null + await this.unsetFlag(SYSTEM_ID, PENDING_DEFENSE_FLAG) + return pending + } + + getEquippedArmorItems() { + return this.items.filter(item => ["armor", "shield"].includes(item.type) && item.system.equipped && !item.system.broken && item.system.armorDie !== "0") + } + + getArmorRollFormula() { + const dice = this.getEquippedArmorItems().map(item => `1${item.system.armorDie}`) + return dice.length ? dice.join(" + ") : "0" + } + + async degradeArmorStep(steps = 1) { + const item = maxDieByRank(this.getEquippedArmorItems(), "armorDie") + if (!item) return null + + const nextDie = MGNERoll.stepDownDie(item.system.armorDie, steps, SYSTEM.armorDice) + const updates = { "system.armorDie": nextDie } + if (nextDie === "0") { + updates["system.broken"] = true + updates["system.equipped"] = false + } + + await item.update(updates) + return { item, nextDie } + } + + async rollAbility(abilityId, options = {}) { + return MGNERoll.promptCheck({ + actor: this, + abilityId, + label: options.label ?? f("MGNE.Roll.CheckLabel", { ability: SYSTEM.abilities[abilityId]?.label ?? abilityId }), + baseDR: options.baseDR ?? 12, + rollType: options.rollType ?? "check", + item: options.item ?? null, + }) + } + + async rollDefense() { + const result = await this.rollAbility("agility", { label: t("MGNE.Roll.DefenseCheck"), rollType: "defense" }) + if (result?.fumble) await this.setPendingDefenseFumble() + return result + } + + async rollWeapon(itemId) { + const item = this.items.get(itemId) + if (!item) return null + if (item.system.broken) { + ui.notifications.warn(f("MGNE.Notification.ItemBroken", { item: item.name })) + return null + } + + const abilityId = item.system.category === "ranged" ? "presence" : "strength" + const result = await this.rollAbility(abilityId, { + label: f("MGNE.Roll.ItemAttackLabel", { item: item.name }), + rollType: "attack", + item, + }) + + if (!result) return null + if (result.fumble) await item.update({ "system.broken": true }) + if (result.critical) { + await this.setPendingDamageBonus(item.id) + } + return result + } + + async rollDamage(itemId) { + const item = this.items.get(itemId) + if (!item) return null + return MGNERoll.rollDamage({ + actor: this, + item, + targetActor: MGNERoll.getFirstTargetActor(), + }) + } + + async rollProfileAttack() { + const attackBaseLabel = normalizeGenericActionLabel(this.system.attack?.label ?? t("MGNE.Common.Attack"), t("MGNE.Common.Attack")) + const attackLabel = formatActionLabel(attackBaseLabel, t("MGNE.Common.Attack")) + const result = await this.rollAbility("strength", { + label: attackLabel, + rollType: "attack", + }) + + if (result?.critical) { + await this.setPendingDamageBonus("profile-attack") + } + + return result + } + + async rollProfileDamage() { + const attackBaseLabel = normalizeGenericActionLabel(this.system.attack?.label ?? t("MGNE.Common.Attack"), t("MGNE.Common.Attack")) + const damageLabel = attackBaseLabel === t("MGNE.Common.Attack") + ? t("MGNE.Common.Damage") + : formatActionLabel(attackBaseLabel, t("MGNE.Common.Damage")) + return MGNERoll.rollFlatDamage({ + actor: this, + label: damageLabel, + formula: this.system.attack?.damage ?? "1", + targetActor: MGNERoll.getFirstTargetActor(), + }) + } + + async rollResonancePerDay({ resetUsed = true, apply = true } = {}) { + const resonanceRoll = await (new Roll("1d4")).evaluate() + const presence = this.system.abilities?.presence?.value ?? 0 + const max = Math.max(1, presence + resonanceRoll.total) + const updates = { "system.resonance.max": max } + if (resetUsed) updates["system.resonance.used"] = 0 + if (apply) await this.update(updates) + return { resonanceRoll, max } + } + + async rollResonation(itemId) { + const item = this.items.get(itemId) + if (!item || item.type !== "resonance-core") return null + if (item.system.burnedOut || item.system.usageDie === "depleted") { + ui.notifications.warn(f("MGNE.Notification.ItemBurnedOut", { item: item.name })) + return null + } + if ((this.system.resonance?.used ?? 0) >= (this.system.resonance?.max ?? 0)) { + ui.notifications.warn(f("MGNE.Notification.ResonancePerDayReached", { actor: this.name })) + return null + } + + const result = await this.rollAbility("presence", { + label: f("MGNE.Roll.InvocationLabel", { item: item.name }), + rollType: "resonance", + item, + }) + + if (!result) return null + + await this.update({ "system.resonance.used": (this.system.resonance.used ?? 0) + 1 }) + + if (!result.success) { + await this.applyDamage(1, { sourceItem: item, ignoreArmor: true, chat: false }) + } + + return result + } + + async rollMorale() { + return MGNERoll.rollMorale(this) + } + + async toggleItemEquipped(itemId) { + const item = this.items.get(itemId) + if (!item) return null + if (item.system.broken) { + ui.notifications.warn(f("MGNE.Notification.ItemBroken", { item: item.name })) + return null + } + return item.update({ "system.equipped": !item.system.equipped }) + } + + async rollUsage(itemId) { + const item = this.items.get(itemId) + if (!item) return null + return item.rollUsage() + } + + async quickRest() { + const roll = await (new Roll("1d4")).evaluate() + const hp = this.system.hp?.value ?? 0 + const hpMax = this.system.hp?.max ?? hp + const healed = Math.max(0, Math.min(hpMax, hp + roll.total) - hp) + const newHp = Math.min(hpMax, hp + roll.total) + + await this.update({ "system.hp.value": newHp }) + await MGNERoll.createRestCard({ + actor: this, + label: f("MGNE.Roll.QuickRestLabel", { actor: this.name }), + subtitle: t("MGNE.Character.QuickRestHelp"), + roll, + outcome: f("MGNE.Roll.RestoredHP", { amount: healed }), + specialText: f("MGNE.Roll.HPNowMax", { hp: newHp, max: hpMax }), + }) + + return { roll, healed, newHp } + } + + async fullRest() { + const hp = this.system.hp?.value ?? 0 + const hpMax = this.system.hp?.max ?? hp + const healRoll = await (new Roll("1d6")).evaluate() + const omenDie = this.system.omens?.die ?? "d2" + const omenRoll = await (new Roll(`1${omenDie}`)).evaluate() + const healed = Math.max(0, Math.min(hpMax, hp + healRoll.total) - hp) + const newHp = Math.min(hpMax, hp + healRoll.total) + + await this.update({ + "system.hp.value": newHp, + "system.omens.current": omenRoll.total, + }) + + await MGNERoll.createRestCard({ + actor: this, + label: f("MGNE.Roll.FullRestLabel", { actor: this.name }), + subtitle: t("MGNE.Character.FullRestHelp"), + roll: healRoll, + outcome: f("MGNE.Roll.RestoredHP", { amount: healed }), + specialText: `${f("MGNE.Roll.HPNowMax", { hp: newHp, max: hpMax })} ${f("MGNE.Roll.OmensReset", { + omens: omenRoll.total, + die: omenDie.toUpperCase(), + roll: omenRoll.total, + })}`, + }) + + return { healRoll, omenRoll, healed, newHp, omens: omenRoll.total } + } + + async syncArtifact(itemId) { + const item = this.items.get(itemId) + if (!item || item.type !== "artifact") return null + + if (item.system.synchronized) { + return item.update({ + "system.synchronized": false, + "system.synchronizedTo": "", + }) + } + + const syncLimit = Math.max(0, this.system.syncLimit ?? this.system.abilities?.toughness?.value ?? 0) + const used = this.system.artifactSync?.used ?? 0 + if (used >= syncLimit) { + ui.notifications.warn(f("MGNE.Notification.CannotSyncMore", { actor: this.name })) + return null + } + + await this.update({ "system.artifactSync.used": used + 1 }) + return item.update({ + "system.synchronized": true, + "system.synchronizedTo": this.id, + }) + } + + async resetDaily() { + const omenDie = this.system.omens?.die ?? "d2" + const omenRoll = await (new Roll(`1${omenDie}`)).evaluate() + const { max: resonanceMax } = await this.rollResonancePerDay({ resetUsed: false, apply: false }) + + await this.update({ + "system.omens.current": omenRoll.total, + "system.resonance.max": resonanceMax, + "system.resonance.used": 0, + "system.artifactSync.used": 0, + "system.survival.salvationUsed": false, + }) + + const syncedArtifacts = this.items.filter(item => item.type === "artifact" && item.system.synchronized) + for (const artifact of syncedArtifacts) { + await artifact.update({ + "system.synchronized": false, + "system.synchronizedTo": "", + }) + } + } + + async applyDamage(amount, { sourceActor = null, sourceItem = null, ignoreArmor = false, critical = false, chat = true } = {}) { + const defenseFumble = await this.consumePendingDefenseFumble() + const incomingDamage = defenseFumble ? amount * 2 : amount + const hp = this.system.hp?.value ?? 0 + let armorRoll = null + let absorbed = 0 + if (!ignoreArmor) { + const formula = this.getArmorRollFormula() + if (formula !== "0") { + armorRoll = await (new Roll(formula)).evaluate() + absorbed = armorRoll.total + } + } + + let appliedDamage = Math.max(0, incomingDamage - absorbed) + let nextHp = hp - appliedDamage + let breakText = "" + let defenseFumbleText = "" + let criticalArmorText = "" + + if (this.type === "character" && hp > 0 && nextHp < 0 && !(this.system.survival?.salvationUsed)) { + nextHp = 0 + appliedDamage = hp + await this.update({ "system.survival.salvationUsed": true }) + } + + if (nextHp <= 0 && hp > 0) { + const breakRoll = await (new Roll("1d4")).evaluate() + breakText = SYSTEM.tables.breakResults[breakRoll.total] ?? "" + } + + await this.update({ "system.hp.value": nextHp }) + + if (defenseFumble) { + const degraded = await this.degradeArmorStep() + defenseFumbleText = degraded + ? f("MGNE.Roll.DefenseFumbleApplied", { item: degraded.item.name, die: degraded.nextDie.toUpperCase() }) + : t("MGNE.Roll.DefenseFumbleNoArmor") + } + + if (critical) { + const degraded = await this.degradeArmorStep() + criticalArmorText = degraded + ? f("MGNE.Roll.ArmorDegradedCritical", { item: degraded.item.name, die: degraded.nextDie.toUpperCase() }) + : t("MGNE.Roll.ArmorNothingToDegrade") + } + + if (chat) { + await MGNERoll.applyDamageCard({ + actor: this, + sourceActor, + sourceItem, + amount: incomingDamage, + armorRoll, + appliedDamage, + newHp: nextHp, + breakText, + defenseFumbleText, + criticalArmorText, + }) + } + + return { + amount: incomingDamage, + absorbed, + appliedDamage, + newHp: nextHp, + breakText, + defenseFumbleText, + criticalArmorText, + summary: f("MGNE.Chat.DamageSummary", { absorbed, appliedDamage, hp: nextHp }), + } + } +} diff --git a/module/documents/combat.mjs b/module/documents/combat.mjs new file mode 100644 index 0000000..c51de8a --- /dev/null +++ b/module/documents/combat.mjs @@ -0,0 +1,139 @@ +import MGNERoll from "./roll.mjs" +import { SYSTEM_ID } from "../config/system.mjs" + +const t = key => game.i18n.localize(key) +const f = (key, data = {}) => game.i18n.format(key, data) +const INITIATIVE_FLAG = "initiativeState" +const SIDE_BONUS = 20 +const FLEE_BASE_TARGET = 5 +const FLEE_MIN_TARGET = 2 + +function isPlayerSide(combatant) { + return Boolean(combatant?.actor?.hasPlayerOwner) +} + +function fleeDialogContent() { + const options = Array.from({ length: FLEE_BASE_TARGET - FLEE_MIN_TARGET + 1 }, (_, index) => { + const value = index + return `` + }).join("") + + return ` +
+
+ + +

${t("MGNE.Combat.StudyHelp")}

+
+
+ ` +} + +export default class MGNECombat extends Combat { + getFleeCombatant(combatantId = null) { + if (combatantId) return this.combatants.get(combatantId) ?? null + + const controlledCombatant = canvas.tokens?.controlled?.[0]?.combatant ?? null + return this.combatant ?? controlledCombatant + } + + async getInitiativeState() { + const existing = this.getFlag(SYSTEM_ID, INITIATIVE_FLAG) + if (existing && Number.isFinite(existing.sideRoll)) return existing + + const sideRoll = await (new Roll("1d6")).evaluate() + const initiativeState = { + sideRoll: sideRoll.total, + playersActFirst: sideRoll.total >= 4, + } + + await this.setFlag(SYSTEM_ID, INITIATIVE_FLAG, initiativeState) + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker(), + content: `

${f("MGNE.Initiative.SideRoll", { roll: initiativeState.sideRoll })}

${t(initiativeState.playersActFirst ? "MGNE.Initiative.PlayersFirst" : "MGNE.Initiative.EnemiesFirst")}

${t("MGNE.Initiative.TieBreak")}

`, + }) + return initiativeState + } + + async rollInitiative(ids, { updateTurn = true } = {}) { + const combatantIds = typeof ids === "string" ? [ids] : Array.from(ids ?? []) + if (!combatantIds.length) return this + + const currentCombatantId = this.combatant?.id ?? null + const initiativeState = await this.getInitiativeState() + const updates = [] + + for (const id of combatantIds) { + const combatant = this.combatants.get(id) + if (!combatant) continue + + const agility = combatant.actor?.system?.abilities?.agility?.value ?? 0 + const roll = await (new Roll("1d6 + @agility", { agility })).evaluate() + const actsWithPriority = isPlayerSide(combatant) === initiativeState.playersActFirst + updates.push({ + _id: id, + initiative: roll.total + (actsWithPriority ? SIDE_BONUS : 0), + }) + } + + if (updates.length) await this.updateEmbeddedDocuments("Combatant", updates) + + if (updateTurn && currentCombatantId) { + const turn = this.turns.findIndex(combatant => combatant.id === currentCombatantId) + if (turn >= 0) await this.update({ turn }) + } + + return this + } + + async rollFlee({ combatantId = null } = {}) { + const combatant = this.getFleeCombatant(combatantId) + if (!combatant?.actor) { + ui.notifications.warn(t("MGNE.Combat.FleeNoCombatant")) + return null + } + + const dialogData = await foundry.applications.api.DialogV2.wait({ + window: { title: t("MGNE.Combat.Flee") }, + classes: ["mgne", "roll-dialog"], + content: fleeDialogContent(), + buttons: [{ + label: t("MGNE.Combat.Flee"), + icon: "fa-solid fa-person-running", + callback: (_event, button) => { + const value = Number.parseInt(button.form?.elements.studyActions?.value ?? "0", 10) + return { studyActions: Math.max(0, Math.min(FLEE_BASE_TARGET - FLEE_MIN_TARGET, value || 0)) } + }, + }], + rejectClose: false, + }) + + if (!dialogData) return null + + const studyActions = dialogData.studyActions ?? 0 + const target = Math.max(FLEE_MIN_TARGET, FLEE_BASE_TARGET - studyActions) + const roll = await (new Roll("1d6")).evaluate() + const escaped = roll.total >= target + + if (escaped) { + await combatant.delete() + } else { + await combatant.actor.update({ "system.hp.value": 0 }) + await combatant.update({ defeated: true }) + } + + await MGNERoll.createActionCard({ + mode: "flee", + actor: combatant.actor, + label: f("MGNE.Roll.FleeLabel", { actor: combatant.name }), + subtitle: f("MGNE.Roll.FleeSubtitle", { target }), + roll, + outcome: t(escaped ? "MGNE.Roll.FleeEscaped" : "MGNE.Roll.FleeKilled"), + specialText: studyActions > 0 + ? f("MGNE.Roll.FleeStudyActions", { count: studyActions }) + : t("MGNE.Roll.FleeNoStudyActions"), + }) + + return { combatant, roll, escaped, target, studyActions } + } +} diff --git a/module/documents/item.mjs b/module/documents/item.mjs new file mode 100644 index 0000000..698697c --- /dev/null +++ b/module/documents/item.mjs @@ -0,0 +1,17 @@ +import MGNERoll from "./roll.mjs" +import { SYSTEM } from "../config/system.mjs" + +export default class MGNEItem extends Item { + prepareBaseData() { + super.prepareBaseData() + const fallbackIcon = SYSTEM.itemTypes[this.type]?.icon + if (!fallbackIcon) return + if (!this._source.img || this._source.img === "icons/svg/item-bag.svg") { + this.updateSource({ img: fallbackIcon }) + } + } + + async rollUsage() { + return MGNERoll.rollUsage(this) + } +} diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs new file mode 100644 index 0000000..fbd1653 --- /dev/null +++ b/module/documents/roll.mjs @@ -0,0 +1,340 @@ +import { SYSTEM, SYSTEM_ID } from "../config/system.mjs" + +const t = key => game.i18n.localize(key) +const f = (key, data = {}) => game.i18n.format(key, data) + +function joinParts(parts) { + return parts.filter(Boolean).join(" ") +} + +function getChatModeLabel(mode) { + return game.i18n.localize(`MGNE.Chat.Mode.${mode}`) || mode +} + +function pickRandom(list = []) { + if (!list.length) return "" + return list[Math.floor(Math.random() * list.length)] +} + +function stepDownDie(die, steps = 1, track = SYSTEM.usageDice) { + let current = die + for (let step = 0; step < steps; step += 1) { + const index = track.indexOf(current) + if (index === -1) return current + current = track[Math.min(index + 1, track.length - 1)] + } + return current +} + +function getFirstTargetActor() { + const target = game.user?.targets ? Array.from(game.user.targets)[0] : null + return target?.actor ?? null +} + +function serializeForm(form) { + return Array.from(form.elements).reduce((acc, element) => { + if (!element.name) return acc + acc[element.name] = element.type === "checkbox" ? element.checked : element.value + return acc + }, {}) +} + +function numericOptions(min, max, current = null) { + const resolvedMin = Math.min(min, Number.isFinite(current) ? current : min) + const resolvedMax = Math.max(max, Number.isFinite(current) ? current : max) + return Array.from({ length: (resolvedMax - resolvedMin) + 1 }, (_, index) => { + const value = resolvedMin + index + return { value, label: String(value) } + }) +} + +async function renderCard(context) { + const outcomeClass = `${context.outcome ?? ""}` + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, "") + const eyebrow = context.eyebrow ?? getChatModeLabel(context.mode ?? "generic") + const normalizedEyebrow = `${eyebrow}`.trim().toLowerCase() + const normalizedLabel = `${context.label ?? ""}`.trim().toLowerCase() + + return foundry.applications.handlebars.renderTemplate(`systems/${SYSTEM_ID}/templates/chat-message.hbs`, { + ...context, + modeClass: context.mode ?? "generic", + eyebrow: "", + outcomeClass, + }) +} + +export default class MGNERoll { + static async promptCheck({ actor, abilityId, label, baseDR = 12, rollType = "check", item = null }) { + const abilityLabel = SYSTEM.abilities[abilityId]?.label ?? abilityId + const content = await foundry.applications.handlebars.renderTemplate(`systems/${SYSTEM_ID}/templates/roll-dialog.hbs`, { + actorName: actor.name, + label, + abilityLabel, + baseDR, + drOptions: numericOptions(6, 20, baseDR), + modifierOptions: numericOptions(-6, 6, 0), + omens: actor.system.omens?.current ?? 0, + rollType, + }) + + const dialogData = await foundry.applications.api.DialogV2.wait({ + window: { title: label }, + classes: ["mgne", "roll-dialog"], + content, + buttons: [ + { + label: t("MGNE.Common.Roll"), + icon: "fa-solid fa-dice-d20", + callback: (_event, button) => serializeForm(button.form), + }, + ], + rejectClose: false, + }) + + if (!dialogData) return null + + const modifier = Number.parseInt(dialogData.modifier ?? 0, 10) || 0 + const spendOmen = Boolean(dialogData.spendOmen) + const dr = (Number.parseInt(dialogData.dr ?? baseDR, 10) || baseDR) - (spendOmen ? 4 : 0) + const abilityValue = actor.system.abilities?.[abilityId]?.value ?? 0 + const sign = modifier >= 0 ? "+" : "-" + const formula = modifier === 0 ? `1d20 + ${abilityValue}` : `1d20 + ${abilityValue} ${sign} ${Math.abs(modifier)}` + const roll = await (new Roll(formula)).evaluate() + const natural = roll.dice?.[0]?.results?.[0]?.result ?? roll.total + + if (spendOmen && (actor.system.omens?.current ?? 0) > 0) { + await actor.update({ "system.omens.current": Math.max(0, actor.system.omens.current - 1) }) + } + + const critical = natural === 20 + const fumble = natural === 1 + const success = critical || (!fumble && roll.total >= dr) + const outcome = critical ? t("MGNE.Roll.OutcomeCriticalSuccess") : fumble ? t("MGNE.Roll.OutcomeFumble") : success ? t("MGNE.Roll.OutcomeSuccess") : t("MGNE.Roll.OutcomeFailure") + + let specialText = "" + if (critical && rollType === "resonance") specialText = pickRandom(SYSTEM.tables.eucatastrophe) + else if (fumble && rollType === "resonance") specialText = pickRandom(SYSTEM.tables.catastrophe) + else if (critical) { + specialText = rollType === "attack" + ? t("MGNE.Roll.CriticalAttack") + : rollType === "defense" + ? t("MGNE.Roll.DefenseCritical") + : pickRandom(SYSTEM.tables.triumphs) + } else if (fumble) { + specialText = rollType === "attack" ? t("MGNE.Roll.AttackFumble") : rollType === "defense" ? t("MGNE.Roll.DefenseFumble") : pickRandom(SYSTEM.tables.mishaps) + } + + const showDamageButton = rollType === "attack" && (success || critical) && !!item + const contentHtml = await renderCard({ + mode: "check", + actorName: actor.name, + actorImg: actor.img, + label, + subtitle: f("MGNE.Roll.CheckSubtitle", { ability: abilityLabel, dr }), + formula: roll.formula, + total: roll.total, + outcome, + specialText, + showDamageButton, + damageActorId: showDamageButton ? actor.id : null, + damageItemId: showDamageButton ? item.id : null, + damageFormula: showDamageButton ? (item.system.damage || "1") : null, + damageCritical: showDamageButton && critical, + }) + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + rolls: [roll], + content: contentHtml, + }) + + return { roll, success, critical, fumble, outcome, specialText, dr, abilityId, item } + } + + static async rollMorale(actor) { + const target = actor.system.morale ?? 7 + const roll = await (new Roll("2d6")).evaluate() + const broken = roll.total >= target + const contentHtml = await renderCard({ + mode: "morale", + actorName: actor.name, + actorImg: actor.img, + label: t("MGNE.Roll.MoraleCheck"), + subtitle: f("MGNE.Roll.TargetSubtitle", { target }), + formula: roll.formula, + total: roll.total, + outcome: broken ? t("MGNE.Roll.OutcomeBroken") : t("MGNE.Roll.OutcomeSteady"), + specialText: broken ? t("MGNE.Roll.MoraleBrokenText") : "", + }) + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + rolls: [roll], + content: contentHtml, + }) + + return { roll, broken } + } + + static async rollDamage({ actor, item }) { + const damageBonus = await actor.consumePendingDamageBonus(item.id) + const multiplier = damageBonus?.multiplier ?? 1 + const baseFormula = item.system.damage || "1" + const formula = multiplier > 1 ? `${multiplier} * (${baseFormula})` : baseFormula + const roll = await (new Roll(formula)).evaluate() + const isCritical = multiplier > 1 + const contentHtml = await renderCard({ + mode: "damage", + actorName: actor.name, + actorImg: actor.img, + label: `${item.name} Damage`, + subtitle: null, + formula: roll.formula, + total: roll.total, + outcome: t("MGNE.Roll.OutcomeRolled"), + specialText: isCritical ? t("MGNE.Roll.CriticalDamageApplied") : "", + showApplyButton: true, + damageTotal: roll.total, + damageCritical: isCritical, + }) + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + rolls: [roll], + content: contentHtml, + }) + + return { roll } + } + + static async rollFlatDamage({ actor, label, formula }) { + const damageBonus = await actor.consumePendingDamageBonus("profile-attack") + const multiplier = damageBonus?.multiplier ?? 1 + const baseFormula = formula || "1" + const resolvedFormula = multiplier > 1 ? `${multiplier} * (${baseFormula})` : baseFormula + const roll = await (new Roll(resolvedFormula)).evaluate() + const isCritical = multiplier > 1 + const contentHtml = await renderCard({ + mode: "damage", + actorName: actor.name, + actorImg: actor.img, + label, + subtitle: null, + formula: roll.formula, + total: roll.total, + outcome: t("MGNE.Roll.OutcomeRolled"), + specialText: isCritical ? t("MGNE.Roll.CriticalDamageApplied") : "", + showApplyButton: true, + damageTotal: roll.total, + damageCritical: isCritical, + }) + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + rolls: [roll], + content: contentHtml, + }) + + return { roll } + } + + static async rollUsage(item) { + const currentDie = item.system.usageDie + if (!currentDie || currentDie === "depleted") { + ui.notifications.warn(f("MGNE.Notification.ItemDepleted", { item: item.name })) + return null + } + + const roll = await (new Roll(`1${currentDie}`)).evaluate() + const depleted = roll.total <= 2 + const nextDie = depleted ? stepDownDie(currentDie) : currentDie + const updates = { "system.usageDie": nextDie } + + if (item.type === "resonance-core" && nextDie === "depleted") updates["system.burnedOut"] = true + await item.update(updates) + + const contentHtml = await renderCard({ + mode: "usage", + actorName: item.parent?.name ?? item.name, + actorImg: item.img, + label: f("MGNE.Roll.ItemUsageLabel", { item: item.name }), + subtitle: f("MGNE.Roll.CurrentDie", { die: currentDie.toUpperCase() }), + formula: roll.formula, + total: roll.total, + outcome: depleted ? f("MGNE.Roll.DowngradedTo", { die: nextDie.toUpperCase() }) : t("MGNE.Roll.NoChange"), + specialText: depleted && nextDie === "depleted" ? t("MGNE.Roll.ItemNowDepleted") : "", + }) + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor: item.parent ?? null }), + rolls: [roll], + content: contentHtml, + }) + + return { roll, depleted, nextDie } + } + + static async applyDamageCard({ actor, sourceActor = null, sourceItem = null, amount, armorRoll = null, appliedDamage, newHp, breakText = "", defenseFumbleText = "", criticalArmorText = "" }) { + const contentHtml = await renderCard({ + mode: "apply-damage", + actorName: actor.name, + actorImg: actor.img, + label: f("MGNE.Roll.TakesDamageLabel", { actor: actor.name }), + subtitle: sourceItem + ? (sourceActor + ? f("MGNE.Roll.DamageSourceWithActor", { item: sourceItem.name, actor: sourceActor.name }) + : f("MGNE.Roll.DamageSourceItem", { item: sourceItem.name })) + : t("MGNE.Roll.DirectDamage"), + formula: armorRoll?.formula ?? "", + total: amount, + outcome: f("MGNE.Roll.HPNow", { hp: newHp }), + specialText: joinParts([ + defenseFumbleText, + armorRoll ? f("MGNE.Roll.ArmorAbsorbed", { amount: armorRoll.total }) : "", + f("MGNE.Roll.AppliedDamageText", { amount: appliedDamage }), + criticalArmorText, + breakText ? f("MGNE.Roll.BreakText", { text: breakText }) : "", + ]), + }) + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + rolls: armorRoll ? [armorRoll] : [], + content: contentHtml, + }) + } + + static async createRestCard({ actor, label, subtitle, roll, outcome, specialText = "" }) { + return this.createActionCard({ mode: "rest", actor, label, subtitle, roll, outcome, specialText }) + } + + static async createActionCard({ mode = "action", actor = null, label, subtitle = "", roll, outcome, specialText = "" }) { + const contentHtml = await renderCard({ + mode, + actorName: actor?.name ?? "", + actorImg: actor?.img ?? "", + label, + subtitle, + formula: roll.formula, + total: roll.total, + outcome, + specialText, + }) + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + rolls: [roll], + content: contentHtml, + }) + } + + static getFirstTargetActor() { + return getFirstTargetActor() + } + + static stepDownDie(die, steps = 1, track = SYSTEM.usageDice) { + return stepDownDie(die, steps, track) + } +} diff --git a/module/models/_module.mjs b/module/models/_module.mjs new file mode 100644 index 0000000..7ed6577 --- /dev/null +++ b/module/models/_module.mjs @@ -0,0 +1,10 @@ +export { default as MGNECharacter } from "./character.mjs" +export { default as MGNECreature } from "./creature.mjs" +export { default as MGNECompanion } from "./companion.mjs" +export { default as MGNEWeapon } from "./weapon.mjs" +export { default as MGNEArmor } from "./armor.mjs" +export { default as MGNEShield } from "./shield.mjs" +export { default as MGNEEquipment } from "./equipment.mjs" +export { default as MGNEResonanceCore } from "./resonance-core.mjs" +export { default as MGNEArtifact } from "./artifact.mjs" +export { default as MGNEFeature } from "./feature.mjs" diff --git a/module/models/armor.mjs b/module/models/armor.mjs new file mode 100644 index 0000000..a3a3cec --- /dev/null +++ b/module/models/armor.mjs @@ -0,0 +1,22 @@ +import { SYSTEM } from "../config/system.mjs" +import { booleanField, htmlField, numberField } from "./shared.mjs" + +export default class MGNEArmor extends foundry.abstract.TypeDataModel { + static defineSchema() { + return { + description: htmlField(""), + armorDie: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "d4", + choices: SYSTEM.armorDieChoices, + }), + penalty: numberField(0, 0, 6), + equipped: booleanField(false), + broken: booleanField(false), + } + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["MGNE.Armor"] +} diff --git a/module/models/artifact.mjs b/module/models/artifact.mjs new file mode 100644 index 0000000..1ce67f1 --- /dev/null +++ b/module/models/artifact.mjs @@ -0,0 +1,28 @@ +import { SYSTEM } from "../config/system.mjs" +import { booleanField, htmlField, stringField } from "./shared.mjs" + +export default class MGNEArtifact extends foundry.abstract.TypeDataModel { + static defineSchema() { + return { + description: htmlField(""), + artifactId: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "shiver-lens", + choices: SYSTEM.artifactChoices, + }), + synchronized: booleanField(false), + synchronizedTo: stringField(""), + usageDie: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "d6", + choices: SYSTEM.usageDieChoices, + }), + broken: booleanField(false), + } + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["MGNE.Artifact"] +} diff --git a/module/models/character.mjs b/module/models/character.mjs new file mode 100644 index 0000000..eb4eab1 --- /dev/null +++ b/module/models/character.mjs @@ -0,0 +1,52 @@ +import { SYSTEM } from "../config/system.mjs" +import { abilitySchema, booleanField, conditionSchema, htmlField, numberField, stringField, trackSchema } from "./shared.mjs" + +export default class MGNECharacter extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + + return { + abilities: abilitySchema(), + hp: trackSchema(1, 1), + omens: new fields.SchemaField({ + current: numberField(0, 0), + die: new fields.StringField({ required: true, nullable: false, initial: "d2", choices: SYSTEM.omenDieChoices }), + }), + resonance: new fields.SchemaField({ + max: numberField(1, 0), + used: numberField(0, 0), + blocked: booleanField(false), + }), + artifactSync: new fields.SchemaField({ + used: numberField(0, 0), + }), + survival: new fields.SchemaField({ + salvationUsed: booleanField(false), + }), + conditions: conditionSchema(), + carryCapacity: numberField(8, 0), + rations: numberField(0, 0), + kiffol: numberField(0, 0), + background: stringField(""), + origin: stringField(""), + scars: stringField(""), + motivation: stringField(""), + vice: stringField(""), + description: htmlField(""), + notes: htmlField(""), + } + } + + prepareDerivedData() { + super.prepareDerivedData() + + this.carryCapacity = (this.abilities.strength?.value ?? 0) + 8 + this.resonance.remaining = Math.max(0, (this.resonance.max ?? 0) - (this.resonance.used ?? 0)) + this.syncLimit = Math.max(0, this.abilities.toughness?.value ?? 0) + this.syncRemaining = Math.max(0, this.syncLimit - (this.artifactSync.used ?? 0)) + this.armorFormula = this.parent?.getArmorRollFormula?.() ?? "0" + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["MGNE.Character"] +} diff --git a/module/models/companion.mjs b/module/models/companion.mjs new file mode 100644 index 0000000..e4f41a6 --- /dev/null +++ b/module/models/companion.mjs @@ -0,0 +1,32 @@ +import { SYSTEM } from "../config/system.mjs" +import { abilitySchema, htmlField, numberField, stringField, trackSchema } from "./shared.mjs" + +export default class MGNECompanion extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + + return { + abilities: abilitySchema(), + hp: trackSchema(1, 1), + morale: numberField(7, 2, 12), + armor: new fields.SchemaField({ + die: new fields.StringField({ required: true, nullable: false, initial: "0", choices: SYSTEM.armorDieChoices }), + }), + attack: new fields.SchemaField({ + label: stringField("Attack"), + damage: stringField("1d4"), + }), + valueText: stringField(""), + traitText: stringField(""), + specialtyText: stringField(""), + adventuringBehavior: htmlField(""), + combatBehavior: htmlField(""), + upkeep: stringField("3d10c per full rest"), + description: htmlField(""), + notes: htmlField(""), + } + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["MGNE.Companion"] +} diff --git a/module/models/creature.mjs b/module/models/creature.mjs new file mode 100644 index 0000000..bd3d877 --- /dev/null +++ b/module/models/creature.mjs @@ -0,0 +1,27 @@ +import { SYSTEM } from "../config/system.mjs" +import { abilitySchema, htmlField, numberField, stringField, trackSchema } from "./shared.mjs" + +export default class MGNECreature extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + + return { + abilities: abilitySchema(), + hp: trackSchema(1, 1), + morale: numberField(7, 2, 12), + armor: new fields.SchemaField({ + die: new fields.StringField({ required: true, nullable: false, initial: "0", choices: SYSTEM.armorDieChoices }), + }), + attack: new fields.SchemaField({ + label: stringField("Attack"), + damage: stringField("1d4"), + }), + description: htmlField(""), + special: htmlField(""), + notes: htmlField(""), + } + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["MGNE.Creature"] +} diff --git a/module/models/equipment.mjs b/module/models/equipment.mjs new file mode 100644 index 0000000..5d251e0 --- /dev/null +++ b/module/models/equipment.mjs @@ -0,0 +1,29 @@ +import { SYSTEM } from "../config/system.mjs" +import { booleanField, htmlField, numberField } from "./shared.mjs" + +export default class MGNEEquipment extends foundry.abstract.TypeDataModel { + static defineSchema() { + return { + description: htmlField(""), + subtype: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "gear", + choices: SYSTEM.equipmentSubtypes, + }), + quantity: numberField(1, 0), + carried: booleanField(true), + equipped: booleanField(false), + usageDie: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "d6", + choices: SYSTEM.usageDieChoices, + }), + consumable: booleanField(false), + } + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["MGNE.Equipment"] +} diff --git a/module/models/feature.mjs b/module/models/feature.mjs new file mode 100644 index 0000000..f5a975c --- /dev/null +++ b/module/models/feature.mjs @@ -0,0 +1,19 @@ +import { SYSTEM } from "../config/system.mjs" +import { htmlField } from "./shared.mjs" + +export default class MGNEFeature extends foundry.abstract.TypeDataModel { + static defineSchema() { + return { + featureId: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "akimbo-hit-priest", + choices: SYSTEM.featureChoices, + }), + description: htmlField(""), + } + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["MGNE.Feature"] +} diff --git a/module/models/resonance-core.mjs b/module/models/resonance-core.mjs new file mode 100644 index 0000000..ca0771a --- /dev/null +++ b/module/models/resonance-core.mjs @@ -0,0 +1,26 @@ +import { SYSTEM } from "../config/system.mjs" +import { booleanField, htmlField, stringField } from "./shared.mjs" + +export default class MGNEResonanceCore extends foundry.abstract.TypeDataModel { + static defineSchema() { + return { + description: htmlField(""), + resonationId: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "accelerate", + choices: SYSTEM.resonanceList, + }), + usageDie: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "d6", + choices: SYSTEM.usageDieChoices, + }), + burnedOut: booleanField(false), + } + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["MGNE.ResonanceCore"] +} diff --git a/module/models/shared.mjs b/module/models/shared.mjs new file mode 100644 index 0000000..a79acce --- /dev/null +++ b/module/models/shared.mjs @@ -0,0 +1,67 @@ +import { SYSTEM } from "../config/system.mjs" + +export function htmlField(initial = "", options = {}) { + return new foundry.data.fields.HTMLField({ required: true, initial, textSearch: true, ...options }) +} + +export function stringField(initial = "", options = {}) { + return new foundry.data.fields.StringField({ required: true, nullable: false, initial, ...options }) +} + +export function numberField(initial = 0, min = 0, max = null, options = {}) { + return new foundry.data.fields.NumberField({ + required: true, + nullable: false, + integer: true, + initial, + min, + ...(max === null ? {} : { max }), + ...options, + }) +} + +export function booleanField(initial = false, options = {}) { + return new foundry.data.fields.BooleanField({ required: true, initial, ...options }) +} + +export function abilitySchema() { + const fields = foundry.data.fields + const schema = {} + for (const abilityId of SYSTEM.abilityOrder) { + const ability = SYSTEM.abilities[abilityId] + schema[abilityId] = new fields.SchemaField( + { + label: stringField(ability.label, { label: "MGNE.Common.Label" }), + value: numberField(0, -3, 6, { label: "MGNE.Common.Value" }), + }, + { label: ability.label } + ) + } + return new fields.SchemaField(schema) +} + +export function conditionSchema() { + const fields = foundry.data.fields + const schema = {} + for (const condition of Object.values(SYSTEM.conditions)) { + if (condition.hasValue) { + schema[condition.id] = new fields.SchemaField( + { value: numberField(0, 0, 12, { label: "MGNE.Common.Value" }) }, + { label: condition.label } + ) + } else { + schema[condition.id] = new fields.SchemaField( + { active: booleanField(false, { label: condition.label }) }, + { label: condition.label } + ) + } + } + return new fields.SchemaField(schema) +} + +export function trackSchema(initialValue = 0, initialMax = 0) { + return new foundry.data.fields.SchemaField({ + value: numberField(initialValue, -99), + max: numberField(initialMax, 0), + }) +} diff --git a/module/models/shield.mjs b/module/models/shield.mjs new file mode 100644 index 0000000..4249887 --- /dev/null +++ b/module/models/shield.mjs @@ -0,0 +1,22 @@ +import { SYSTEM } from "../config/system.mjs" +import { booleanField, htmlField, numberField } from "./shared.mjs" + +export default class MGNEShield extends foundry.abstract.TypeDataModel { + static defineSchema() { + return { + description: htmlField(""), + armorDie: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "d4", + choices: SYSTEM.armorDieChoices, + }), + penalty: numberField(0, 0, 4), + equipped: booleanField(false), + broken: booleanField(false), + } + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["MGNE.Shield"] +} diff --git a/module/models/weapon.mjs b/module/models/weapon.mjs new file mode 100644 index 0000000..b38d727 --- /dev/null +++ b/module/models/weapon.mjs @@ -0,0 +1,31 @@ +import { SYSTEM } from "../config/system.mjs" +import { booleanField, htmlField, numberField, stringField } from "./shared.mjs" + +export default class MGNEWeapon extends foundry.abstract.TypeDataModel { + static defineSchema() { + return { + description: htmlField(""), + category: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "melee", + choices: SYSTEM.weaponCategories, + }), + damage: stringField("1d4"), + range: stringField("Touch"), + properties: stringField(""), + usageDie: new foundry.data.fields.StringField({ + required: true, + nullable: false, + initial: "d6", + choices: SYSTEM.usageDieChoices, + }), + quantity: numberField(1, 0), + equipped: booleanField(false), + broken: booleanField(false), + } + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["MGNE.Weapon"] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..06a2821 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1239 @@ +{ + "name": "fvtt-machine-gods-noxian-expanse", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fvtt-machine-gods-noxian-expanse", + "version": "0.1.0", + "license": "UNLICENSED", + "devDependencies": { + "@foundryvtt/foundryvtt-cli": "^3.0.3", + "less": "^4.6.4" + } + }, + "node_modules/@foundryvtt/foundryvtt-cli": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@foundryvtt/foundryvtt-cli/-/foundryvtt-cli-3.0.3.tgz", + "integrity": "sha512-byNLOrZ9ev6PfW+gflhonT5Y+Goq2aHERwVGWO3eYRKez+lzZJoqMY/YMEi54XYozzWWn8d6LuRbBsVPJ30haw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "classic-level": "^1.4.1", + "esm": "^3.2.25", + "js-yaml": "^4.1.0", + "mkdirp": "^3.0.1", + "nedb-promises": "^6.2.3", + "yargs": "^17.7.2" + }, + "bin": { + "fvtt": "fvtt.mjs" + }, + "engines": { + "node": ">17.0.0" + } + }, + "node_modules/@seald-io/binary-search-tree": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz", + "integrity": "sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA==", + "dev": true + }, + "node_modules/@seald-io/nedb": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@seald-io/nedb/-/nedb-4.1.2.tgz", + "integrity": "sha512-bDr6TqjBVS2rDyYM9CPxAnotj5FuNL9NF8o7h7YyFXM7yruqT4ddr+PkSb2mJvvw991bqdftazkEo38gykvaww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@seald-io/binary-search-tree": "^1.0.3", + "localforage": "^1.10.0", + "util": "^0.12.5" + } + }, + "node_modules/abstract-level": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.4.tgz", + "integrity": "sha512-eUP/6pbXBkMbXFdx4IH2fVgvB7M0JvR7/lIL33zcs0IBcwjdzSSl31TOJsaCzmKSSDF9h8QYSOJux4Nd4YJqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "catering": "^2.1.0", + "is-buffer": "^2.0.5", + "level-supports": "^4.0.0", + "level-transcoder": "^1.0.1", + "module-error": "^1.0.1", + "queue-microtask": "^1.2.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/catering": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz", + "integrity": "sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/classic-level": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.4.1.tgz", + "integrity": "sha512-qGx/KJl3bvtOHrGau2WklEZuXhS3zme+jf+fsu6Ej7W7IP/C49v7KNlWIsT1jZu0YnfzSIYDGcEWpCa1wKGWXQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "abstract-level": "^1.0.2", + "catering": "^2.1.0", + "module-error": "^1.0.1", + "napi-macros": "^2.2.2", + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/less": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/less/-/less-4.6.4.tgz", + "integrity": "sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^3.0.5", + "parse-node-version": "^1.0.1" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/level-supports": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-4.0.1.tgz", + "integrity": "sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/level-transcoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-transcoder/-/level-transcoder-1.0.1.tgz", + "integrity": "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "module-error": "^1.0.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/module-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz", + "integrity": "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/napi-macros": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/nedb-promises": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/nedb-promises/-/nedb-promises-6.2.3.tgz", + "integrity": "sha512-enq0IjNyBz9Qy9W/QPCcLGh/QORGBjXbIeZeWvIjO3OMLyAvlKT3hiJubP2BKEiFniUlR3L01o18ktqgn5jxqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@seald-io/nedb": "^4.0.2" + } + }, + "node_modules/needle": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.5.0.tgz", + "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..22dfbbd --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "fvtt-machine-gods-noxian-expanse", + "private": true, + "version": "0.1.0", + "type": "module", + "description": "FoundryVTT system for Machine Gods of the Noxian Expanse", + "license": "UNLICENSED", + "scripts": { + "build:css": "lessc less/mgne.less css/mgne.css", + "watch:css": "lessc --watch less/mgne.less css/mgne.css", + "build:packs": "node ./tools/packCompendiums.mjs", + "unpack:packs": "node ./tools/unpackCompendiums.mjs", + "build": "npm run build:css && npm run build:packs" + }, + "devDependencies": { + "@foundryvtt/foundryvtt-cli": "^3.0.3", + "less": "^4.6.4" + } +} diff --git a/packs-system/armor/000015.log b/packs-system/armor/000015.log new file mode 100644 index 0000000..e69de29 diff --git a/packs-system/armor/000017.ldb b/packs-system/armor/000017.ldb new file mode 100644 index 0000000..41762b5 Binary files /dev/null and b/packs-system/armor/000017.ldb differ diff --git a/packs-system/armor/CURRENT b/packs-system/armor/CURRENT new file mode 100644 index 0000000..625d147 --- /dev/null +++ b/packs-system/armor/CURRENT @@ -0,0 +1 @@ +MANIFEST-000013 diff --git a/packs-system/armor/LOCK b/packs-system/armor/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/packs-system/armor/LOG b/packs-system/armor/LOG new file mode 100644 index 0000000..e07d439 --- /dev/null +++ b/packs-system/armor/LOG @@ -0,0 +1,15 @@ +2026/05/05-13:48:23.563581 7f3501fec6c0 Recovering log #10 +2026/05/05-13:48:23.574685 7f3501fec6c0 Delete type=3 #8 +2026/05/05-13:48:23.574743 7f3501fec6c0 Delete type=0 #10 +2026/05/05-13:48:23.577629 7f35017eb6c0 Level-0 table #16: started +2026/05/05-13:48:23.580670 7f35017eb6c0 Level-0 table #16: 1137 bytes OK +2026/05/05-13:48:23.586557 7f35017eb6c0 Delete type=0 #14 +2026/05/05-13:48:23.586666 7f35017eb6c0 Manual compaction at level-0 from '!items!mgne-arm-chainshirt' @ 72057594037927935 : 1 .. '!items!mgne-shd-medshield' @ 0 : 0; will stop at (end) +2026/05/05-13:48:23.586686 7f35017eb6c0 Manual compaction at level-1 from '!items!mgne-arm-chainshirt' @ 72057594037927935 : 1 .. '!items!mgne-shd-medshield' @ 0 : 0; will stop at '!items!mgne-shd-medshield' @ 23 : 1 +2026/05/05-13:48:23.586692 7f35017eb6c0 Compacting 1@1 + 1@2 files +2026/05/05-13:48:23.589637 7f35017eb6c0 Generated table #17@1: 8 keys, 1137 bytes +2026/05/05-13:48:23.589645 7f35017eb6c0 Compacted 1@1 + 1@2 files => 1137 bytes +2026/05/05-13:48:23.596288 7f35017eb6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/05/05-13:48:23.596382 7f35017eb6c0 Delete type=2 #12 +2026/05/05-13:48:23.596475 7f35017eb6c0 Delete type=2 #16 +2026/05/05-13:48:23.596542 7f35017eb6c0 Manual compaction at level-1 from '!items!mgne-shd-medshield' @ 23 : 1 .. '!items!mgne-shd-medshield' @ 0 : 0; will stop at (end) diff --git a/packs-system/armor/LOG.old b/packs-system/armor/LOG.old new file mode 100644 index 0000000..fc4fde8 --- /dev/null +++ b/packs-system/armor/LOG.old @@ -0,0 +1,15 @@ +2026/05/05-13:39:51.776003 7f1d84bfc6c0 Recovering log #7 +2026/05/05-13:39:51.786391 7f1d84bfc6c0 Delete type=0 #7 +2026/05/05-13:39:51.786461 7f1d84bfc6c0 Delete type=3 #6 +2026/05/05-13:39:51.789446 7f1d67fff6c0 Level-0 table #11: started +2026/05/05-13:39:51.792965 7f1d67fff6c0 Level-0 table #11: 1137 bytes OK +2026/05/05-13:39:51.799016 7f1d67fff6c0 Delete type=0 #9 +2026/05/05-13:39:51.799186 7f1d67fff6c0 Manual compaction at level-0 from '!items!mgne-arm-chainshirt' @ 72057594037927935 : 1 .. '!items!mgne-shd-medshield' @ 0 : 0; will stop at (end) +2026/05/05-13:39:51.799216 7f1d67fff6c0 Manual compaction at level-1 from '!items!mgne-arm-chainshirt' @ 72057594037927935 : 1 .. '!items!mgne-shd-medshield' @ 0 : 0; will stop at '!items!mgne-shd-medshield' @ 15 : 1 +2026/05/05-13:39:51.799223 7f1d67fff6c0 Compacting 1@1 + 1@2 files +2026/05/05-13:39:51.802308 7f1d67fff6c0 Generated table #12@1: 8 keys, 1137 bytes +2026/05/05-13:39:51.802320 7f1d67fff6c0 Compacted 1@1 + 1@2 files => 1137 bytes +2026/05/05-13:39:51.809042 7f1d67fff6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/05/05-13:39:51.809132 7f1d67fff6c0 Delete type=2 #5 +2026/05/05-13:39:51.809219 7f1d67fff6c0 Delete type=2 #11 +2026/05/05-13:39:51.809304 7f1d67fff6c0 Manual compaction at level-1 from '!items!mgne-shd-medshield' @ 15 : 1 .. '!items!mgne-shd-medshield' @ 0 : 0; will stop at (end) diff --git a/packs-system/armor/MANIFEST-000013 b/packs-system/armor/MANIFEST-000013 new file mode 100644 index 0000000..c6cb3cf Binary files /dev/null and b/packs-system/armor/MANIFEST-000013 differ diff --git a/packs-system/companions/000015.log b/packs-system/companions/000015.log new file mode 100644 index 0000000..e69de29 diff --git a/packs-system/companions/000017.ldb b/packs-system/companions/000017.ldb new file mode 100644 index 0000000..1b6869e Binary files /dev/null and b/packs-system/companions/000017.ldb differ diff --git a/packs-system/companions/CURRENT b/packs-system/companions/CURRENT new file mode 100644 index 0000000..625d147 --- /dev/null +++ b/packs-system/companions/CURRENT @@ -0,0 +1 @@ +MANIFEST-000013 diff --git a/packs-system/companions/LOCK b/packs-system/companions/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/packs-system/companions/LOG b/packs-system/companions/LOG new file mode 100644 index 0000000..f2c6941 --- /dev/null +++ b/packs-system/companions/LOG @@ -0,0 +1,15 @@ +2026/05/05-13:48:23.599377 7f3501fec6c0 Recovering log #10 +2026/05/05-13:48:23.608831 7f3501fec6c0 Delete type=3 #8 +2026/05/05-13:48:23.608871 7f3501fec6c0 Delete type=0 #10 +2026/05/05-13:48:23.610126 7f35017eb6c0 Level-0 table #16: started +2026/05/05-13:48:23.613188 7f35017eb6c0 Level-0 table #16: 2595 bytes OK +2026/05/05-13:48:23.619068 7f35017eb6c0 Delete type=0 #14 +2026/05/05-13:48:23.619177 7f35017eb6c0 Manual compaction at level-0 from '!actors!mgne-comp-beguiled-noble' @ 72057594037927935 : 1 .. '!actors!mgne-comp-silicon-cantor' @ 0 : 0; will stop at (end) +2026/05/05-13:48:23.619194 7f35017eb6c0 Manual compaction at level-1 from '!actors!mgne-comp-beguiled-noble' @ 72057594037927935 : 1 .. '!actors!mgne-comp-silicon-cantor' @ 0 : 0; will stop at '!actors!mgne-comp-silicon-cantor' @ 12 : 1 +2026/05/05-13:48:23.619198 7f35017eb6c0 Compacting 1@1 + 1@2 files +2026/05/05-13:48:23.622633 7f35017eb6c0 Generated table #17@1: 4 keys, 2595 bytes +2026/05/05-13:48:23.622658 7f35017eb6c0 Compacted 1@1 + 1@2 files => 2595 bytes +2026/05/05-13:48:23.629056 7f35017eb6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/05/05-13:48:23.629142 7f35017eb6c0 Delete type=2 #12 +2026/05/05-13:48:23.629277 7f35017eb6c0 Delete type=2 #16 +2026/05/05-13:48:23.629353 7f35017eb6c0 Manual compaction at level-1 from '!actors!mgne-comp-silicon-cantor' @ 12 : 1 .. '!actors!mgne-comp-silicon-cantor' @ 0 : 0; will stop at (end) diff --git a/packs-system/companions/LOG.old b/packs-system/companions/LOG.old new file mode 100644 index 0000000..801c662 --- /dev/null +++ b/packs-system/companions/LOG.old @@ -0,0 +1,15 @@ +2026/05/05-13:39:51.812159 7f1d84bfc6c0 Recovering log #7 +2026/05/05-13:39:51.821606 7f1d84bfc6c0 Delete type=0 #7 +2026/05/05-13:39:51.821673 7f1d84bfc6c0 Delete type=3 #6 +2026/05/05-13:39:51.822659 7f1d67fff6c0 Level-0 table #11: started +2026/05/05-13:39:51.826191 7f1d67fff6c0 Level-0 table #11: 2595 bytes OK +2026/05/05-13:39:51.832550 7f1d67fff6c0 Delete type=0 #9 +2026/05/05-13:39:51.832709 7f1d67fff6c0 Manual compaction at level-0 from '!actors!mgne-comp-beguiled-noble' @ 72057594037927935 : 1 .. '!actors!mgne-comp-silicon-cantor' @ 0 : 0; will stop at (end) +2026/05/05-13:39:51.832732 7f1d67fff6c0 Manual compaction at level-1 from '!actors!mgne-comp-beguiled-noble' @ 72057594037927935 : 1 .. '!actors!mgne-comp-silicon-cantor' @ 0 : 0; will stop at '!actors!mgne-comp-silicon-cantor' @ 8 : 1 +2026/05/05-13:39:51.832738 7f1d67fff6c0 Compacting 1@1 + 1@2 files +2026/05/05-13:39:51.835888 7f1d67fff6c0 Generated table #12@1: 4 keys, 2595 bytes +2026/05/05-13:39:51.835899 7f1d67fff6c0 Compacted 1@1 + 1@2 files => 2595 bytes +2026/05/05-13:39:51.842063 7f1d67fff6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/05/05-13:39:51.842118 7f1d67fff6c0 Delete type=2 #5 +2026/05/05-13:39:51.842232 7f1d67fff6c0 Delete type=2 #11 +2026/05/05-13:39:51.842372 7f1d67fff6c0 Manual compaction at level-1 from '!actors!mgne-comp-silicon-cantor' @ 8 : 1 .. '!actors!mgne-comp-silicon-cantor' @ 0 : 0; will stop at (end) diff --git a/packs-system/companions/MANIFEST-000013 b/packs-system/companions/MANIFEST-000013 new file mode 100644 index 0000000..e3a5d14 Binary files /dev/null and b/packs-system/companions/MANIFEST-000013 differ diff --git a/packs-system/features/000015.log b/packs-system/features/000015.log new file mode 100644 index 0000000..e69de29 diff --git a/packs-system/features/000017.ldb b/packs-system/features/000017.ldb new file mode 100644 index 0000000..ffcc7d8 Binary files /dev/null and b/packs-system/features/000017.ldb differ diff --git a/packs-system/features/CURRENT b/packs-system/features/CURRENT new file mode 100644 index 0000000..625d147 --- /dev/null +++ b/packs-system/features/CURRENT @@ -0,0 +1 @@ +MANIFEST-000013 diff --git a/packs-system/features/LOCK b/packs-system/features/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/packs-system/features/LOG b/packs-system/features/LOG new file mode 100644 index 0000000..1228e8e --- /dev/null +++ b/packs-system/features/LOG @@ -0,0 +1,15 @@ +2026/05/05-13:48:23.633079 7f3501fec6c0 Recovering log #10 +2026/05/05-13:48:23.642339 7f3501fec6c0 Delete type=3 #8 +2026/05/05-13:48:23.642401 7f3501fec6c0 Delete type=0 #10 +2026/05/05-13:48:23.643704 7f35017eb6c0 Level-0 table #16: started +2026/05/05-13:48:23.647322 7f35017eb6c0 Level-0 table #16: 8604 bytes OK +2026/05/05-13:48:23.653855 7f35017eb6c0 Delete type=0 #14 +2026/05/05-13:48:23.653991 7f35017eb6c0 Manual compaction at level-0 from '!items!mgne-feat-11' @ 72057594037927935 : 1 .. '!items!mgne-feat-66' @ 0 : 0; will stop at (end) +2026/05/05-13:48:23.654009 7f35017eb6c0 Manual compaction at level-1 from '!items!mgne-feat-11' @ 72057594037927935 : 1 .. '!items!mgne-feat-66' @ 0 : 0; will stop at '!items!mgne-feat-66' @ 108 : 1 +2026/05/05-13:48:23.654014 7f35017eb6c0 Compacting 1@1 + 1@2 files +2026/05/05-13:48:23.657283 7f35017eb6c0 Generated table #17@1: 36 keys, 8604 bytes +2026/05/05-13:48:23.657309 7f35017eb6c0 Compacted 1@1 + 1@2 files => 8604 bytes +2026/05/05-13:48:23.663300 7f35017eb6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/05/05-13:48:23.663409 7f35017eb6c0 Delete type=2 #12 +2026/05/05-13:48:23.663527 7f35017eb6c0 Delete type=2 #16 +2026/05/05-13:48:23.663604 7f35017eb6c0 Manual compaction at level-1 from '!items!mgne-feat-66' @ 108 : 1 .. '!items!mgne-feat-66' @ 0 : 0; will stop at (end) diff --git a/packs-system/features/LOG.old b/packs-system/features/LOG.old new file mode 100644 index 0000000..95c7472 --- /dev/null +++ b/packs-system/features/LOG.old @@ -0,0 +1,15 @@ +2026/05/05-13:39:51.846381 7f1d84bfc6c0 Recovering log #7 +2026/05/05-13:39:51.856790 7f1d84bfc6c0 Delete type=0 #7 +2026/05/05-13:39:51.856839 7f1d84bfc6c0 Delete type=3 #6 +2026/05/05-13:39:51.857932 7f1d67fff6c0 Level-0 table #11: started +2026/05/05-13:39:51.861193 7f1d67fff6c0 Level-0 table #11: 8605 bytes OK +2026/05/05-13:39:51.868312 7f1d67fff6c0 Delete type=0 #9 +2026/05/05-13:39:51.868475 7f1d67fff6c0 Manual compaction at level-0 from '!items!mgne-feat-11' @ 72057594037927935 : 1 .. '!items!mgne-feat-66' @ 0 : 0; will stop at (end) +2026/05/05-13:39:51.868501 7f1d67fff6c0 Manual compaction at level-1 from '!items!mgne-feat-11' @ 72057594037927935 : 1 .. '!items!mgne-feat-66' @ 0 : 0; will stop at '!items!mgne-feat-66' @ 72 : 1 +2026/05/05-13:39:51.868507 7f1d67fff6c0 Compacting 1@1 + 1@2 files +2026/05/05-13:39:51.871749 7f1d67fff6c0 Generated table #12@1: 36 keys, 8605 bytes +2026/05/05-13:39:51.871757 7f1d67fff6c0 Compacted 1@1 + 1@2 files => 8605 bytes +2026/05/05-13:39:51.877669 7f1d67fff6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/05/05-13:39:51.877798 7f1d67fff6c0 Delete type=2 #5 +2026/05/05-13:39:51.877917 7f1d67fff6c0 Delete type=2 #11 +2026/05/05-13:39:51.878042 7f1d67fff6c0 Manual compaction at level-1 from '!items!mgne-feat-66' @ 72 : 1 .. '!items!mgne-feat-66' @ 0 : 0; will stop at (end) diff --git a/packs-system/features/MANIFEST-000013 b/packs-system/features/MANIFEST-000013 new file mode 100644 index 0000000..56860a1 Binary files /dev/null and b/packs-system/features/MANIFEST-000013 differ diff --git a/packs-system/resonations/000015.log b/packs-system/resonations/000015.log new file mode 100644 index 0000000..e69de29 diff --git a/packs-system/resonations/000017.ldb b/packs-system/resonations/000017.ldb new file mode 100644 index 0000000..4d18924 Binary files /dev/null and b/packs-system/resonations/000017.ldb differ diff --git a/packs-system/resonations/CURRENT b/packs-system/resonations/CURRENT new file mode 100644 index 0000000..625d147 --- /dev/null +++ b/packs-system/resonations/CURRENT @@ -0,0 +1 @@ +MANIFEST-000013 diff --git a/packs-system/resonations/LOCK b/packs-system/resonations/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/packs-system/resonations/LOG b/packs-system/resonations/LOG new file mode 100644 index 0000000..95bd70b --- /dev/null +++ b/packs-system/resonations/LOG @@ -0,0 +1,15 @@ +2026/05/05-13:48:23.666925 7f3501fec6c0 Recovering log #10 +2026/05/05-13:48:23.677741 7f3501fec6c0 Delete type=3 #8 +2026/05/05-13:48:23.677801 7f3501fec6c0 Delete type=0 #10 +2026/05/05-13:48:23.679383 7f35017eb6c0 Level-0 table #16: started +2026/05/05-13:48:23.682550 7f35017eb6c0 Level-0 table #16: 6014 bytes OK +2026/05/05-13:48:23.688697 7f35017eb6c0 Delete type=0 #14 +2026/05/05-13:48:23.688835 7f35017eb6c0 Manual compaction at level-0 from '!items!mgne-res-accelerate' @ 72057594037927935 : 1 .. '!items!mgne-res-summonmist' @ 0 : 0; will stop at (end) +2026/05/05-13:48:23.688857 7f35017eb6c0 Manual compaction at level-1 from '!items!mgne-res-accelerate' @ 72057594037927935 : 1 .. '!items!mgne-res-summonmist' @ 0 : 0; will stop at '!items!mgne-res-summonmist' @ 60 : 1 +2026/05/05-13:48:23.688863 7f35017eb6c0 Compacting 1@1 + 1@2 files +2026/05/05-13:48:23.692761 7f35017eb6c0 Generated table #17@1: 20 keys, 6014 bytes +2026/05/05-13:48:23.692776 7f35017eb6c0 Compacted 1@1 + 1@2 files => 6014 bytes +2026/05/05-13:48:23.699601 7f35017eb6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/05/05-13:48:23.699680 7f35017eb6c0 Delete type=2 #12 +2026/05/05-13:48:23.699761 7f35017eb6c0 Delete type=2 #16 +2026/05/05-13:48:23.699842 7f35017eb6c0 Manual compaction at level-1 from '!items!mgne-res-summonmist' @ 60 : 1 .. '!items!mgne-res-summonmist' @ 0 : 0; will stop at (end) diff --git a/packs-system/resonations/LOG.old b/packs-system/resonations/LOG.old new file mode 100644 index 0000000..5c1ea2e --- /dev/null +++ b/packs-system/resonations/LOG.old @@ -0,0 +1,15 @@ +2026/05/05-13:39:51.881037 7f1d84bfc6c0 Recovering log #7 +2026/05/05-13:39:51.891266 7f1d84bfc6c0 Delete type=0 #7 +2026/05/05-13:39:51.891342 7f1d84bfc6c0 Delete type=3 #6 +2026/05/05-13:39:51.892391 7f1d67fff6c0 Level-0 table #11: started +2026/05/05-13:39:51.895586 7f1d67fff6c0 Level-0 table #11: 5952 bytes OK +2026/05/05-13:39:51.901902 7f1d67fff6c0 Delete type=0 #9 +2026/05/05-13:39:51.902058 7f1d67fff6c0 Manual compaction at level-0 from '!items!mgne-res-accelerate' @ 72057594037927935 : 1 .. '!items!mgne-res-summonmist' @ 0 : 0; will stop at (end) +2026/05/05-13:39:51.902081 7f1d67fff6c0 Manual compaction at level-1 from '!items!mgne-res-accelerate' @ 72057594037927935 : 1 .. '!items!mgne-res-summonmist' @ 0 : 0; will stop at '!items!mgne-res-summonmist' @ 40 : 1 +2026/05/05-13:39:51.902087 7f1d67fff6c0 Compacting 1@1 + 1@2 files +2026/05/05-13:39:51.905491 7f1d67fff6c0 Generated table #12@1: 20 keys, 5952 bytes +2026/05/05-13:39:51.905502 7f1d67fff6c0 Compacted 1@1 + 1@2 files => 5952 bytes +2026/05/05-13:39:51.911576 7f1d67fff6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/05/05-13:39:51.911663 7f1d67fff6c0 Delete type=2 #5 +2026/05/05-13:39:51.911773 7f1d67fff6c0 Delete type=2 #11 +2026/05/05-13:39:51.911882 7f1d67fff6c0 Manual compaction at level-1 from '!items!mgne-res-summonmist' @ 40 : 1 .. '!items!mgne-res-summonmist' @ 0 : 0; will stop at (end) diff --git a/packs-system/resonations/MANIFEST-000013 b/packs-system/resonations/MANIFEST-000013 new file mode 100644 index 0000000..9128a61 Binary files /dev/null and b/packs-system/resonations/MANIFEST-000013 differ diff --git a/packs-system/weapons/000015.log b/packs-system/weapons/000015.log new file mode 100644 index 0000000..e69de29 diff --git a/packs-system/weapons/000017.ldb b/packs-system/weapons/000017.ldb new file mode 100644 index 0000000..de78627 Binary files /dev/null and b/packs-system/weapons/000017.ldb differ diff --git a/packs-system/weapons/CURRENT b/packs-system/weapons/CURRENT new file mode 100644 index 0000000..625d147 --- /dev/null +++ b/packs-system/weapons/CURRENT @@ -0,0 +1 @@ +MANIFEST-000013 diff --git a/packs-system/weapons/LOCK b/packs-system/weapons/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/packs-system/weapons/LOG b/packs-system/weapons/LOG new file mode 100644 index 0000000..82c04cb --- /dev/null +++ b/packs-system/weapons/LOG @@ -0,0 +1,15 @@ +2026/05/05-13:48:23.701768 7f3501fec6c0 Recovering log #10 +2026/05/05-13:48:23.711409 7f3501fec6c0 Delete type=3 #8 +2026/05/05-13:48:23.711469 7f3501fec6c0 Delete type=0 #10 +2026/05/05-13:48:23.712391 7f35017eb6c0 Level-0 table #16: started +2026/05/05-13:48:23.716390 7f35017eb6c0 Level-0 table #16: 1243 bytes OK +2026/05/05-13:48:23.722430 7f35017eb6c0 Delete type=0 #14 +2026/05/05-13:48:23.722572 7f35017eb6c0 Manual compaction at level-0 from '!items!mgne-wpn-club' @ 72057594037927935 : 1 .. '!items!mgne-wpn-whip' @ 0 : 0; will stop at (end) +2026/05/05-13:48:23.722593 7f35017eb6c0 Manual compaction at level-1 from '!items!mgne-wpn-club' @ 72057594037927935 : 1 .. '!items!mgne-wpn-whip' @ 0 : 0; will stop at '!items!mgne-wpn-whip' @ 36 : 1 +2026/05/05-13:48:23.722599 7f35017eb6c0 Compacting 1@1 + 1@2 files +2026/05/05-13:48:23.725674 7f35017eb6c0 Generated table #17@1: 12 keys, 1243 bytes +2026/05/05-13:48:23.725685 7f35017eb6c0 Compacted 1@1 + 1@2 files => 1243 bytes +2026/05/05-13:48:23.732539 7f35017eb6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/05/05-13:48:23.732618 7f35017eb6c0 Delete type=2 #12 +2026/05/05-13:48:23.732712 7f35017eb6c0 Delete type=2 #16 +2026/05/05-13:48:23.732795 7f35017eb6c0 Manual compaction at level-1 from '!items!mgne-wpn-whip' @ 36 : 1 .. '!items!mgne-wpn-whip' @ 0 : 0; will stop at (end) diff --git a/packs-system/weapons/LOG.old b/packs-system/weapons/LOG.old new file mode 100644 index 0000000..5b965d9 --- /dev/null +++ b/packs-system/weapons/LOG.old @@ -0,0 +1,15 @@ +2026/05/05-13:39:51.914413 7f1d84bfc6c0 Recovering log #7 +2026/05/05-13:39:51.925184 7f1d84bfc6c0 Delete type=0 #7 +2026/05/05-13:39:51.925233 7f1d84bfc6c0 Delete type=3 #6 +2026/05/05-13:39:51.926185 7f1d67fff6c0 Level-0 table #11: started +2026/05/05-13:39:51.929331 7f1d67fff6c0 Level-0 table #11: 1243 bytes OK +2026/05/05-13:39:51.935508 7f1d67fff6c0 Delete type=0 #9 +2026/05/05-13:39:51.935697 7f1d67fff6c0 Manual compaction at level-0 from '!items!mgne-wpn-club' @ 72057594037927935 : 1 .. '!items!mgne-wpn-whip' @ 0 : 0; will stop at (end) +2026/05/05-13:39:51.935720 7f1d67fff6c0 Manual compaction at level-1 from '!items!mgne-wpn-club' @ 72057594037927935 : 1 .. '!items!mgne-wpn-whip' @ 0 : 0; will stop at '!items!mgne-wpn-whip' @ 24 : 1 +2026/05/05-13:39:51.935725 7f1d67fff6c0 Compacting 1@1 + 1@2 files +2026/05/05-13:39:51.939171 7f1d67fff6c0 Generated table #12@1: 12 keys, 1243 bytes +2026/05/05-13:39:51.939200 7f1d67fff6c0 Compacted 1@1 + 1@2 files => 1243 bytes +2026/05/05-13:39:51.945741 7f1d67fff6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/05/05-13:39:51.945832 7f1d67fff6c0 Delete type=2 #5 +2026/05/05-13:39:51.945950 7f1d67fff6c0 Delete type=2 #11 +2026/05/05-13:39:51.946046 7f1d67fff6c0 Manual compaction at level-1 from '!items!mgne-wpn-whip' @ 24 : 1 .. '!items!mgne-wpn-whip' @ 0 : 0; will stop at (end) diff --git a/packs-system/weapons/MANIFEST-000013 b/packs-system/weapons/MANIFEST-000013 new file mode 100644 index 0000000..7fd42ee Binary files /dev/null and b/packs-system/weapons/MANIFEST-000013 differ diff --git a/packs_src/armor/Chain_Shirt.yaml b/packs_src/armor/Chain_Shirt.yaml new file mode 100644 index 0000000..cfcd019 --- /dev/null +++ b/packs_src/armor/Chain_Shirt.yaml @@ -0,0 +1,11 @@ +_id: mgne-arm-chainshirt +_key: "!items!mgne-arm-chainshirt" +name: "Chain Shirt" +type: armor +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/armor.svg +system: + armorDie: "d6" + currentDie: "d6" + penalty: -1 + description: "Interlocked metal rings. Good protection but heavy. Agility -1 DR." + equipped: false diff --git a/packs_src/armor/Clothing_Average.yaml b/packs_src/armor/Clothing_Average.yaml new file mode 100644 index 0000000..20c8d07 --- /dev/null +++ b/packs_src/armor/Clothing_Average.yaml @@ -0,0 +1,11 @@ +_id: mgne-arm-clothing-avg +_key: "!items!mgne-arm-clothing-avg" +name: "Clothing (Average)" +type: armor +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/armor.svg +system: + armorDie: "d2" + currentDie: "d2" + penalty: 0 + description: "Basic clothing offering minimal protection." + equipped: false diff --git a/packs_src/armor/Full_Plate.yaml b/packs_src/armor/Full_Plate.yaml new file mode 100644 index 0000000..8ac3537 --- /dev/null +++ b/packs_src/armor/Full_Plate.yaml @@ -0,0 +1,11 @@ +_id: mgne-arm-fullplate +_key: "!items!mgne-arm-fullplate" +name: "Full Plate" +type: armor +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/armor.svg +system: + armorDie: "d10" + currentDie: "d10" + penalty: -3 + description: "Complete metal encasement. Maximum protection. Agility -3 DR." + equipped: false diff --git a/packs_src/armor/Gambeson.yaml b/packs_src/armor/Gambeson.yaml new file mode 100644 index 0000000..cab07d5 --- /dev/null +++ b/packs_src/armor/Gambeson.yaml @@ -0,0 +1,11 @@ +_id: mgne-arm-gambeson +_key: "!items!mgne-arm-gambeson" +name: "Gambeson" +type: armor +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/armor.svg +system: + armorDie: "d2" + currentDie: "d2" + penalty: 0 + description: "Padded cloth armor. Light and flexible." + equipped: false diff --git a/packs_src/armor/Half_Plate.yaml b/packs_src/armor/Half_Plate.yaml new file mode 100644 index 0000000..b5a5682 --- /dev/null +++ b/packs_src/armor/Half_Plate.yaml @@ -0,0 +1,11 @@ +_id: mgne-arm-halfplate +_key: "!items!mgne-arm-halfplate" +name: "Half Plate" +type: armor +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/armor.svg +system: + armorDie: "d8" + currentDie: "d8" + penalty: -2 + description: "Heavy metal plates covering vital areas. Agility -2 DR." + equipped: false diff --git a/packs_src/armor/Helm.yaml b/packs_src/armor/Helm.yaml new file mode 100644 index 0000000..a535e79 --- /dev/null +++ b/packs_src/armor/Helm.yaml @@ -0,0 +1,11 @@ +_id: mgne-arm-helm +_key: "!items!mgne-arm-helm" +name: "Helm" +type: armor +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/armor.svg +system: + armorDie: "d2" + currentDie: "d2" + penalty: 0 + description: "A protective head covering. Bonus armor item." + equipped: false diff --git a/packs_src/armor/Medium_Shield.yaml b/packs_src/armor/Medium_Shield.yaml new file mode 100644 index 0000000..5820003 --- /dev/null +++ b/packs_src/armor/Medium_Shield.yaml @@ -0,0 +1,11 @@ +_id: mgne-shd-medshield +_key: "!items!mgne-shd-medshield" +name: "Medium Shield" +type: shield +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/shield.svg +system: + armorDie: "d4" + currentDie: "d4" + penalty: -1 + description: "A sturdy wooden and metal shield. Can be broken to negate all damage from one attack. Agility -1 DR." + equipped: false diff --git a/packs_src/armor/Padded_Leather.yaml b/packs_src/armor/Padded_Leather.yaml new file mode 100644 index 0000000..ae4d183 --- /dev/null +++ b/packs_src/armor/Padded_Leather.yaml @@ -0,0 +1,11 @@ +_id: mgne-arm-paddedleather +_key: "!items!mgne-arm-paddedleather" +name: "Padded Leather" +type: armor +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/armor.svg +system: + armorDie: "d4" + currentDie: "d4" + penalty: 0 + description: "Hardened leather over padding. Moderate protection." + equipped: false diff --git a/packs_src/companions/Beguiled_Noble.yaml b/packs_src/companions/Beguiled_Noble.yaml new file mode 100644 index 0000000..49fe360 --- /dev/null +++ b/packs_src/companions/Beguiled_Noble.yaml @@ -0,0 +1,22 @@ +_id: mgne-comp-beguiled-noble +_key: "!actors!mgne-comp-beguiled-noble" +name: "Beguiled Noble" +type: companion +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/companion.svg +system: + hp: + value: 6 + max: 6 + morale: 9 + armor: + die: "d4" + attack: + label: "Blade" + damage: "1d6" + description: "While they could live a comfortable life in the city, they have recently become taken with the concept of adventure." + special: "They Value: Telling stories, Gambling, Intoxication, Prudent economic theory, Learning new skills, Obtaining exotic animals. + +Trait: Porcelain mask of an emotional face / Smokes sweet herbs from an opium pipe / Coat glimmers with threaded lights / Carries a skull, speaks to it. + +Specialty: Long credit line (covers 2D10₵ per day) / Armor protection increased by +1 / Convinced of own invincibility / Knows the names of minor peers in most cities." + notes: "" diff --git a/packs_src/companions/Dustland_Mercenary.yaml b/packs_src/companions/Dustland_Mercenary.yaml new file mode 100644 index 0000000..ff75cc8 --- /dev/null +++ b/packs_src/companions/Dustland_Mercenary.yaml @@ -0,0 +1,22 @@ +_id: mgne-comp-dustland-mercenary +_key: "!actors!mgne-comp-dustland-mercenary" +name: "Dustland Mercenary" +type: companion +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/companion.svg +system: + hp: + value: 8 + max: 8 + morale: 7 + armor: + die: "d4" + attack: + label: "Cleaver" + damage: "1d4+2" + description: "On the road between becoming a bandit and renouncing banditry, the mercenary walks." + special: "They Value: A full belly, A warm fire, Dead enemies, Dental care, Reassurance of life after death, Being told that they're special. + +Trait: Refuses to hone blades / Wears a patch over a good eye to see in the dark / Sleeps standing up, snores / Rusting teeth. + +Specialty (roll D4): 1-in-4 chance of picking unnecessary fights / Eats twice as much / Terrified of Artifacts, leaves if you have any / Constantly shopping for a better deal." + notes: "" diff --git a/packs_src/companions/Scrapling_Pickpocket.yaml b/packs_src/companions/Scrapling_Pickpocket.yaml new file mode 100644 index 0000000..a582fbd --- /dev/null +++ b/packs_src/companions/Scrapling_Pickpocket.yaml @@ -0,0 +1,22 @@ +_id: mgne-comp-scrapling-pickpocket +_key: "!actors!mgne-comp-scrapling-pickpocket" +name: "Scrapling Pickpocket" +type: companion +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/companion.svg +system: + hp: + value: 3 + max: 3 + morale: 4 + armor: + die: "" + attack: + label: "Folding Knife" + damage: "1d2" + description: "A pitiful bit of flesh, tearing pieces of sustenance from this ruined world." + special: "They Value: Prompt payment, Roasted mushrooms, Being read to from a book of prayers, Near death experiences, Cleanliness, The company of protectors. + +Trait: Has fleas / Squeaky, distracting voice / Hides parts of meals around camp / Beady eyed stare. + +Specialty: 1-in-4 chance of tripling damage / 1-in-2 chance of finding 50% more money / Gets hit by traps instead of you / Squeezes into small gaps." + notes: "" diff --git a/packs_src/companions/Silicon_Cantor.yaml b/packs_src/companions/Silicon_Cantor.yaml new file mode 100644 index 0000000..bd09875 --- /dev/null +++ b/packs_src/companions/Silicon_Cantor.yaml @@ -0,0 +1,22 @@ +_id: mgne-comp-silicon-cantor +_key: "!actors!mgne-comp-silicon-cantor" +name: "Silicon Cantor" +type: companion +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/companion.svg +system: + hp: + value: 4 + max: 4 + morale: 6 + armor: + die: "" + attack: + label: "Book Thump" + damage: "1" + description: "A freelance preacher, quick to profess faith in any god whose territory they have blundered into." + special: "They Value: Wants to buy your fingerbones, Holy brandings, Collections of ancient music, Circular debates, A thimble of wine before bed, Peace and quiet. + +Trait: Strident voice but weak doctrinal knowledge / Sings prayers off-key / Cabinet of relics, all clever forgeries / Ferments various vegetables, carries them in jars. + +Specialty: Knows a random Resonation by heart (can invoke D4 times/day) / Bonded to an Artifact (always Synchronized) / Basic healing training (D6 times/day, restore 1 HP) / Welcomed into settlements even hostile ones." + notes: "" diff --git a/packs_src/features/Akimbo_Hit_Priest.yaml b/packs_src/features/Akimbo_Hit_Priest.yaml new file mode 100644 index 0000000..171266b --- /dev/null +++ b/packs_src/features/Akimbo_Hit_Priest.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-11 +_key: "!items!mgne-feat-11" +name: "Akimbo Hit Priest" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "On the direct orders of your god, you kill in the most stylish way possible. Once per Round, after hitting with an attack, you may suffer Fatigued (1) to attack again." + featureId: "11" diff --git a/packs_src/features/Ambivalent_Slouch.yaml b/packs_src/features/Ambivalent_Slouch.yaml new file mode 100644 index 0000000..a086f68 --- /dev/null +++ b/packs_src/features/Ambivalent_Slouch.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-12 +_key: "!items!mgne-feat-12" +name: "Ambivalent Slouch" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "The world is always demanding so much of you. It's easier just to go with the flow. You gain +1 Carrying Capacity. You reduce the amount armor and shields penalize your Agility DR by 1. When you break a shield to reduce damage from an attack, you gain +D6 damage to your next attack that combat." + featureId: "12" diff --git a/packs_src/features/Archival_Shambler.yaml b/packs_src/features/Archival_Shambler.yaml new file mode 100644 index 0000000..1238c95 --- /dev/null +++ b/packs_src/features/Archival_Shambler.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-13 +_key: "!items!mgne-feat-13" +name: "Archival Shambler" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Long days spent immersed in the lores of forgotten peoples have not left you particularly equipped for socializing. There is a 2-in-6 chance you can translate any given work or speak any given language. When relying on raw knowledge to solve a problem, gain -1 DR. When trying to influence others, suffer +1 DR." + featureId: "13" diff --git a/packs_src/features/Aureate_Sangrist.yaml b/packs_src/features/Aureate_Sangrist.yaml new file mode 100644 index 0000000..415a9bc --- /dev/null +++ b/packs_src/features/Aureate_Sangrist.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-14 +_key: "!items!mgne-feat-14" +name: "Aureate Sangrist" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "The spray of blood reassures you and reminds you of your place in the world—namely, holding the knife. Against Bleeding targets, you also score crits on a 19. Once per combat, when you strike a target, you may inflict Bleeding (1)." + featureId: "14" diff --git a/packs_src/features/Avaricious_Gubbingrifter.yaml b/packs_src/features/Avaricious_Gubbingrifter.yaml new file mode 100644 index 0000000..028442a --- /dev/null +++ b/packs_src/features/Avaricious_Gubbingrifter.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-15 +_key: "!items!mgne-feat-15" +name: "Avaricious Gubbingrifter" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "All these little things, they stick in your pockets. Gain three random Resonance Cores. Gain -1 DR to invoking Resonations. When someone tries to take one of your Resonance Cores, you always know." + featureId: "15" diff --git a/packs_src/features/Backbiter.yaml b/packs_src/features/Backbiter.yaml new file mode 100644 index 0000000..9bba51f --- /dev/null +++ b/packs_src/features/Backbiter.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-16 +_key: "!items!mgne-feat-16" +name: "Backbiter" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Whenever you see a gap in someone's guard, your fingers itch. When you win the Initiative roll, during the first Round of combat your attacks bypass 2D6 Armor. When you bypass all of a target's Armor, you deal +2 damage." + featureId: "16" diff --git a/packs_src/features/Ballisteering_Ancilader.yaml b/packs_src/features/Ballisteering_Ancilader.yaml new file mode 100644 index 0000000..686d030 --- /dev/null +++ b/packs_src/features/Ballisteering_Ancilader.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-21 +_key: "!items!mgne-feat-21" +name: "Ballisteering Ancilader" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "When making a ranged attack, you may reduce a corresponding quiver or bullet pouch's Usage Die up to six times to add +1 damage or -1 DR each. After the attack, you suffer Stunned (1)." + featureId: "21" diff --git a/packs_src/features/Biohound.yaml b/packs_src/features/Biohound.yaml new file mode 100644 index 0000000..a6171d7 --- /dev/null +++ b/packs_src/features/Biohound.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-22 +_key: "!items!mgne-feat-22" +name: "Biohound" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Your face has been modified to track targets at long range. So long as you don't switch to tracking a new prey, you can never lose the scent of a creature you've drawn blood from. When sublimating your sense of self and desires to act under the orders of another, you gain -1 DR to checks." + featureId: "22" diff --git a/packs_src/features/Branded_Pyrebearer.yaml b/packs_src/features/Branded_Pyrebearer.yaml new file mode 100644 index 0000000..d7efdd1 --- /dev/null +++ b/packs_src/features/Branded_Pyrebearer.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-23 +_key: "!items!mgne-feat-23" +name: "Branded Pyrebearer" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Tasked with the penance of dragging a smoldering ceramic cart from village to village, you eventually grew to hear whispers from the flame. Your Carrying Capacity increases by +2. Once per combat, when you deal 5 or more damage with an attack, you may also inflict Burning." + featureId: "23" diff --git a/packs_src/features/Brawneous_Gigant.yaml b/packs_src/features/Brawneous_Gigant.yaml new file mode 100644 index 0000000..211b0ad --- /dev/null +++ b/packs_src/features/Brawneous_Gigant.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-24 +_key: "!items!mgne-feat-24" +name: "Brawneous Gigant" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "You were trained, grown, or modified for draft work. Your Carrying Capacity increases by +4 and your HP by +1. Your Strength cannot decrease during Improvement. When confronted with fire, you must check Toughness DR 12 or suffer Stunned (2) permanently until you get away from it." + featureId: "24" diff --git a/packs_src/features/Chart_Reading_Maniac.yaml b/packs_src/features/Chart_Reading_Maniac.yaml new file mode 100644 index 0000000..5af77f4 --- /dev/null +++ b/packs_src/features/Chart_Reading_Maniac.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-25 +_key: "!items!mgne-feat-25" +name: "Chart Reading Maniac" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "You have an additional Exploration Turn per day, which you may gift to another character. When you would become lost, there is a 3-in-6 chance you instead find your way. Once per day, you may reroll an exploration or ruin table." + featureId: "25" diff --git a/packs_src/features/Decrepit_Wheezeblessed.yaml b/packs_src/features/Decrepit_Wheezeblessed.yaml new file mode 100644 index 0000000..162a99f --- /dev/null +++ b/packs_src/features/Decrepit_Wheezeblessed.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-26 +_key: "!items!mgne-feat-26" +name: "Decrepit Wheezeblessed" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Your lightheadedness gives you visions of the future. Add +1 to your Omens Die and -1 DR to anticipating danger. Suffer Fatigued (1) after any period of exertion, such as combat." + featureId: "26" diff --git a/packs_src/features/Fleetrazor.yaml b/packs_src/features/Fleetrazor.yaml new file mode 100644 index 0000000..0a16af7 --- /dev/null +++ b/packs_src/features/Fleetrazor.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-31 +_key: "!items!mgne-feat-31" +name: "Fleetrazor" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Gain -1 DR to hiding. When in shadows, gain a further -1 DR. When you strike from concealment against an unsuspecting target, deal +D4 damage. When running away from a fight you caused, gain +1 to the Fleeing D6." + featureId: "31" diff --git a/packs_src/features/Forsaken_Vintner.yaml b/packs_src/features/Forsaken_Vintner.yaml new file mode 100644 index 0000000..cc9d92c --- /dev/null +++ b/packs_src/features/Forsaken_Vintner.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-32 +_key: "!items!mgne-feat-32" +name: "Forsaken Vintner" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Once per full rest, you may turn D6 Rations into a bottle of brew. You may carry brew up to your Presence +1 (minimum 1). During your Turn, before attempting a check, you may swig a brew to set your roll to a 12—after the check resolves, you suffer Stunned (1). Once per combat, when you suffer Burning, you may automatically extinguish it." + featureId: "32" diff --git a/packs_src/features/Gremlitic_Ostracite.yaml b/packs_src/features/Gremlitic_Ostracite.yaml new file mode 100644 index 0000000..e342997 --- /dev/null +++ b/packs_src/features/Gremlitic_Ostracite.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-33 +_key: "!items!mgne-feat-33" +name: "Gremlitic Ostracite" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "When you touch technological objects, there is a 2-in-6 chance they break. When you touch a technological creature, it suffers D4 damage bypassing Armor. When you touch a machine god, roll a D20. On a 1, the god explodes. This only happens the first time you touch it." + featureId: "33" diff --git a/packs_src/features/Hephaestine_Axewright.yaml b/packs_src/features/Hephaestine_Axewright.yaml new file mode 100644 index 0000000..739bdd1 --- /dev/null +++ b/packs_src/features/Hephaestine_Axewright.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-34 +_key: "!items!mgne-feat-34" +name: "Hephaestine Axewright" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "When you have an hour to modify a weapon, you may add a Property to it. Weapons may have no more than one Property added this way. When in your hands, a weapon's Durability is one Usage Die size higher than they would normally be—if you hand a weapon with a Durability of D4 to someone else, it falls apart instantly." + featureId: "34" diff --git a/packs_src/features/Homing_Dove.yaml b/packs_src/features/Homing_Dove.yaml new file mode 100644 index 0000000..ecdbf5f --- /dev/null +++ b/packs_src/features/Homing_Dove.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-35 +_key: "!items!mgne-feat-35" +name: "Homing Dove" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Your mind records places you have visited in alarming detail. So long as it is physically possible, you always know how to return to somewhere you have been. Also, you can search any place you have been regardless of whether you are currently there (mental image only; you must physically return to affect it)." + featureId: "35" diff --git a/packs_src/features/Insufferant_Gloryhog.yaml b/packs_src/features/Insufferant_Gloryhog.yaml new file mode 100644 index 0000000..37a8e44 --- /dev/null +++ b/packs_src/features/Insufferant_Gloryhog.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-36 +_key: "!items!mgne-feat-36" +name: "Insufferant Gloryhog" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "You gain -2 DR to giving orders to your sworn underlings and +2 DR to giving them to anyone else. Once per day, when an underling backstabs you in some way, you may have them recover an Omen." + featureId: "36" diff --git a/packs_src/features/Kismet_Dancer.yaml b/packs_src/features/Kismet_Dancer.yaml new file mode 100644 index 0000000..2e63732 --- /dev/null +++ b/packs_src/features/Kismet_Dancer.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-41 +_key: "!items!mgne-feat-41" +name: "Kismet Dancer" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "You have learned one of the highly acrobatic folk-plays of the Expanse, the performance of which is said to bring luck to viewers. Add +1 to your Omens Die, but when you hit 0 Omens you suffer D4 Fatigued. You gain -2 DR to giving performances." + featureId: "41" diff --git a/packs_src/features/Knellringer.yaml b/packs_src/features/Knellringer.yaml new file mode 100644 index 0000000..b6676a9 --- /dev/null +++ b/packs_src/features/Knellringer.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-42 +_key: "!items!mgne-feat-42" +name: "Knellringer" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "You gain +2 Carrying Capacity. At the start of combat, you may designate a foe. If you kill them, you gain +D2 damage on attacks for the rest of combat." + featureId: "42" diff --git a/packs_src/features/Lapidarists_Ocular.yaml b/packs_src/features/Lapidarists_Ocular.yaml new file mode 100644 index 0000000..547c73a --- /dev/null +++ b/packs_src/features/Lapidarists_Ocular.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-43 +_key: "!items!mgne-feat-43" +name: "Lapidarist's Ocular" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Once per day, you may roll twice and take the best result when asked to roll the Usage Die of an item. When rolling to generate a random Artifact, you may roll twice and take the result you prefer. You may invoke Resonations +2 times per day." + featureId: "43" diff --git a/packs_src/features/Liturgicantal_Blesswell.yaml b/packs_src/features/Liturgicantal_Blesswell.yaml new file mode 100644 index 0000000..8351571 --- /dev/null +++ b/packs_src/features/Liturgicantal_Blesswell.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-44 +_key: "!items!mgne-feat-44" +name: "Liturgicantal Blesswell" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "D4 times per full rest, you may restore D4 Hit Points to a character. Once per day, you may restore a Usage Die by a dice step, up to its starting value. You have -1 DR to administering medical care." + featureId: "44" diff --git a/packs_src/features/Mauled_Coaxcreature.yaml b/packs_src/features/Mauled_Coaxcreature.yaml new file mode 100644 index 0000000..0749724 --- /dev/null +++ b/packs_src/features/Mauled_Coaxcreature.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-45 +_key: "!items!mgne-feat-45" +name: "Mauled Coaxcreature" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Once per time you flee, you may reroll the D6. The first time you take damage each combat, you gain an extra D4 Armor against it." + featureId: "45" diff --git a/packs_src/features/Moss_Dealer.yaml b/packs_src/features/Moss_Dealer.yaml new file mode 100644 index 0000000..89f5d62 --- /dev/null +++ b/packs_src/features/Moss_Dealer.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-46 +_key: "!items!mgne-feat-46" +name: "Moss Dealer" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "D6 times per day, you may coat a weapon in plant slime or administer a dose to a willing creature. Upon dealing damage with the coated weapon or administering the dose, there is a 3-in-6 chance the target is healed D2 HP. Otherwise, they are Poisoned." + featureId: "46" diff --git a/packs_src/features/Pummelbully.yaml b/packs_src/features/Pummelbully.yaml new file mode 100644 index 0000000..80d4cbd --- /dev/null +++ b/packs_src/features/Pummelbully.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-51 +_key: "!items!mgne-feat-51" +name: "Pummelbully" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "You deal D6 damage with unarmed attacks, and the first time you land one each Round you inflict Stunned (1)." + featureId: "51" diff --git a/packs_src/features/Rat_In_Steel_Tunnels.yaml b/packs_src/features/Rat_In_Steel_Tunnels.yaml new file mode 100644 index 0000000..6f3fd9a --- /dev/null +++ b/packs_src/features/Rat_In_Steel_Tunnels.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-52 +_key: "!items!mgne-feat-52" +name: "Rat In Steel Tunnels" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "You can see in the dark, provided there is even a trace quantity of light. You gain -1 DR to listening. You suffer temporary Blinded with a D4 Usage Die when exposed to bright light." + featureId: "52" diff --git a/packs_src/features/Scopesinger.yaml b/packs_src/features/Scopesinger.yaml new file mode 100644 index 0000000..9d20e46 --- /dev/null +++ b/packs_src/features/Scopesinger.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-53 +_key: "!items!mgne-feat-53" +name: "Scopesinger" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "When you make an attack, you may begin to sing, suffering +4 DR. If the attack then hits, it deals maximum damage. If it misses, you gain -1 DR to your next attack that combat." + featureId: "53" diff --git a/packs_src/features/Scraper_Clamber.yaml b/packs_src/features/Scraper_Clamber.yaml new file mode 100644 index 0000000..85fcaa7 --- /dev/null +++ b/packs_src/features/Scraper_Clamber.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-54 +_key: "!items!mgne-feat-54" +name: "Scraper Clamber" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "You can climb perilous surfaces with ease, only needing to check in unusual conditions (high winds, sudden earthquake, under attack, etc.). You gain -4 DR when you do need to check for climbing." + featureId: "54" diff --git a/packs_src/features/Scurrilous_Wretch.yaml b/packs_src/features/Scurrilous_Wretch.yaml new file mode 100644 index 0000000..2339b47 --- /dev/null +++ b/packs_src/features/Scurrilous_Wretch.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-55 +_key: "!items!mgne-feat-55" +name: "Scurrilous Wretch" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Once per Round, when you damage a creature, you may suffer Stunned (1) to force them to check Morale. Your own Companions are treated as 1 Morale higher, to a maximum of 11 Morale." + featureId: "55" diff --git a/packs_src/features/Sewnshut.yaml b/packs_src/features/Sewnshut.yaml new file mode 100644 index 0000000..b248850 --- /dev/null +++ b/packs_src/features/Sewnshut.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-56 +_key: "!items!mgne-feat-56" +name: "Sewnshut" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Your tongue has been taken from you. Add +1 to your Omens Die. When confronting believers, once per day you may reroll any result that would determine their disposition towards you. You can intone and make noises, but you cannot form words." + featureId: "56" diff --git a/packs_src/features/Siphon_Leech.yaml b/packs_src/features/Siphon_Leech.yaml new file mode 100644 index 0000000..7c2e58f --- /dev/null +++ b/packs_src/features/Siphon_Leech.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-61 +_key: "!items!mgne-feat-61" +name: "Siphon Leech" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Instead of consuming a Ration, you may deal d6 damage to a willing or restrained subject. At any time, you may choose to start Bleeding (D2). While Bleeding, you deal +1 damage with attacks. When you remove Bleeding, provided you took at least 1 damage from it, you recover 1 HP." + featureId: "61" diff --git a/packs_src/features/Skittish_Chipwright.yaml b/packs_src/features/Skittish_Chipwright.yaml new file mode 100644 index 0000000..31d83f2 --- /dev/null +++ b/packs_src/features/Skittish_Chipwright.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-62 +_key: "!items!mgne-feat-62" +name: "Skittish Chipwright" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "At any time, you may destroy two Artifacts to roll for a new Artifact or two Resonance Cores to roll for a new Resonance Core. When manipulating technology to do something it was not designed for, you gain -1 DR." + featureId: "62" diff --git a/packs_src/features/Starborn_Beknighted.yaml b/packs_src/features/Starborn_Beknighted.yaml new file mode 100644 index 0000000..310d9fc --- /dev/null +++ b/packs_src/features/Starborn_Beknighted.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-63 +_key: "!items!mgne-feat-63" +name: "Starborn Beknighted" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "Once per visit to the Catastrophe Table and Mishaps Table, you may roll twice and take the preferred result. Twice per day, when you would fail to invoke a Resonation, you may instead succeed and take D2 damage bypassing Armor." + featureId: "63" diff --git a/packs_src/features/Undulating_Combatant.yaml b/packs_src/features/Undulating_Combatant.yaml new file mode 100644 index 0000000..c7750c0 --- /dev/null +++ b/packs_src/features/Undulating_Combatant.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-64 +_key: "!items!mgne-feat-64" +name: "Undulating Combatant" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "You gain -1 DR to attacks with halberds, staves, spears, and whips. You gain -1 DR to attacking while fighting in a hazardous environment. You gain -2 DR to keeping your balance." + featureId: "64" diff --git a/packs_src/features/Walking_Skeleton.yaml b/packs_src/features/Walking_Skeleton.yaml new file mode 100644 index 0000000..7a75c21 --- /dev/null +++ b/packs_src/features/Walking_Skeleton.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-65 +_key: "!items!mgne-feat-65" +name: "Walking Skeleton" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "When you consume a Ration or other meal, there is a 3-in-6 chance you are able to get your nourishment from a few crumbs, allowing you to conserve the rest of the meal. You gain -1 DR to slipping through bars and narrow gaps." + featureId: "65" diff --git a/packs_src/features/Worm_Enthusiast.yaml b/packs_src/features/Worm_Enthusiast.yaml new file mode 100644 index 0000000..072db2a --- /dev/null +++ b/packs_src/features/Worm_Enthusiast.yaml @@ -0,0 +1,8 @@ +_id: mgne-feat-66 +_key: "!items!mgne-feat-66" +name: "Worm Enthusiast" +type: feature +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/feature.svg +system: + description: "During your Turn, you may freely stand from Prone without consequences. You gain -1 DR to wriggling out of Restrained. You can freely produce a handful of worms in situations where this is useful." + featureId: "66" diff --git a/packs_src/resonations/Accelerate.yaml b/packs_src/resonations/Accelerate.yaml new file mode 100644 index 0000000..df2d005 --- /dev/null +++ b/packs_src/resonations/Accelerate.yaml @@ -0,0 +1,13 @@ +_id: mgne-res-accelerate +_key: "!items!mgne-res-accelerate" +name: Accelerate +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + You allow a target to move out of step with time. Touched target gains an + additional Action at the end of the Round, then suffers D4 damage bypassing + Armor from chronal tearing. Cannot be invoked more than once per Round. If + used on yourself, it does not cost an Action. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Blast.yaml b/packs_src/resonations/Blast.yaml new file mode 100644 index 0000000..ce5db85 --- /dev/null +++ b/packs_src/resonations/Blast.yaml @@ -0,0 +1,11 @@ +_id: mgne-res-blast +_key: "!items!mgne-res-blast" +name: Blast +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + Terrible energy rips from your hands. Pick a target within line of sight. + They take D12 damage and you and your target are both Poisoned and Infected. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Breathe_Water.yaml b/packs_src/resonations/Breathe_Water.yaml new file mode 100644 index 0000000..40766da --- /dev/null +++ b/packs_src/resonations/Breathe_Water.yaml @@ -0,0 +1,13 @@ +_id: mgne-res-breathewater +_key: "!items!mgne-res-breathewater" +name: Breathe Water +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + Your face is enclosed in a thin shell of atmosphere. You can breathe in + hostile environments and ignore crushing pressure and vacuum for 4D10 + minutes, after which point you can freely sustain it by spending one of + your remaining invocations per day. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Cauterize.yaml b/packs_src/resonations/Cauterize.yaml new file mode 100644 index 0000000..812aca9 --- /dev/null +++ b/packs_src/resonations/Cauterize.yaml @@ -0,0 +1,12 @@ +_id: mgne-res-cauterize +_key: "!items!mgne-res-cauterize" +name: Cauterize +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + With a searing touch, you cure all maladies. Touch a target, deal 1 damage + bypassing Armor to them, then remove Bleeding, Blinded, Fatigued, Infected, + Poisoned, Restrained, and Stunned from them. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Create_Illusion.yaml b/packs_src/resonations/Create_Illusion.yaml new file mode 100644 index 0000000..8c5d486 --- /dev/null +++ b/packs_src/resonations/Create_Illusion.yaml @@ -0,0 +1,15 @@ +_id: mgne-res-createillusion +_key: "!items!mgne-res-createillusion" +name: Create Illusion +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + By sculpting hardlight, you create a convincing multisensory phantasm. The + illusion may be no larger than a room. For others, recognizing that it is an + illusion without interacting with it requires a Presence check (DR 18). After + interacting with it, they gain -7 DR to recognizing its falsity. The illusion + lasts a day, at which point you may freely sustain it by spending one of your + invocations from the next day. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Distract.yaml b/packs_src/resonations/Distract.yaml new file mode 100644 index 0000000..9ec54a4 --- /dev/null +++ b/packs_src/resonations/Distract.yaml @@ -0,0 +1,11 @@ +_id: mgne-res-distract +_key: "!items!mgne-res-distract" +name: Distract +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + A horrible spray of colors and sensations confounds your target. Pick a + target within range of sight. They suffer Blinded with a D4 Usage Die. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Eagle_Eye.yaml b/packs_src/resonations/Eagle_Eye.yaml new file mode 100644 index 0000000..16874a3 --- /dev/null +++ b/packs_src/resonations/Eagle_Eye.yaml @@ -0,0 +1,13 @@ +_id: mgne-res-eagleeye +_key: "!items!mgne-res-eagleeye" +name: Eagle Eye +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + A small bird made of plasma alights from your palm, then journeys off to + chart distant hemispheres. You can see through the eyes of your bird. It + moves no faster than a walking pace, but is unaffected by wind and gravity. + It sheds bright light and lasts for ten minutes. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Empower_Weapon.yaml b/packs_src/resonations/Empower_Weapon.yaml new file mode 100644 index 0000000..9ad1e50 --- /dev/null +++ b/packs_src/resonations/Empower_Weapon.yaml @@ -0,0 +1,11 @@ +_id: mgne-res-empower +_key: "!items!mgne-res-empower" +name: Empower Weapon +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + You wreathe a held object in additional force. Touched weapon deals +2 + damage for D6 Rounds. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Fireball.yaml b/packs_src/resonations/Fireball.yaml new file mode 100644 index 0000000..53cf4b3 --- /dev/null +++ b/packs_src/resonations/Fireball.yaml @@ -0,0 +1,11 @@ +_id: mgne-res-fireball +_key: "!items!mgne-res-fireball" +name: Fireball +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + A burst of flames blasts your target and everything around them. Designate + D6 nearby targets within line of sight. They suffer D6 damage and Burning. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Hover.yaml b/packs_src/resonations/Hover.yaml new file mode 100644 index 0000000..e5ea122 --- /dev/null +++ b/packs_src/resonations/Hover.yaml @@ -0,0 +1,14 @@ +_id: mgne-res-hover +_key: "!items!mgne-res-hover" +name: Hover +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + You fly, although no faster than a jog and with the roar of an overloading + transformer emanating as you do. You can fly for 3D10 minutes. During this + time, you have +10 Carrying Capacity and can treat one Heavy object as + Light. When this effect ends, you can freely sustain it by spending one of + your remaining invocations per day. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Influence_Mind.yaml b/packs_src/resonations/Influence_Mind.yaml new file mode 100644 index 0000000..e066fdb --- /dev/null +++ b/packs_src/resonations/Influence_Mind.yaml @@ -0,0 +1,15 @@ +_id: mgne-res-influencemind +_key: "!items!mgne-res-influencemind" +name: Influence Mind +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + The flicker of energy through a creature's synapses is no more special than + any other form of electricity. By reaching out, you can twist it into a more + beneficial shape. Pick a target within line of sight. They obey a single + order to the best of their ability. If the order is outright suicidal or + viscerally contravenes their morality, they may take D8 damage bypassing + Armor instead. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Knit_Flesh.yaml b/packs_src/resonations/Knit_Flesh.yaml new file mode 100644 index 0000000..0707ae0 --- /dev/null +++ b/packs_src/resonations/Knit_Flesh.yaml @@ -0,0 +1,13 @@ +_id: mgne-res-knitflesh +_key: "!items!mgne-res-knitflesh" +name: Knit Flesh +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + A simple mental goad reroutes bloodflow and hormones, building new tissue. + Transfer D4-1 (minimum 1) points between a touched target's Agility, + Strength, and Toughness until their next full rest. Alternatively, deal D8 + damage bypassing Armor. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Light_Construct.yaml b/packs_src/resonations/Light_Construct.yaml new file mode 100644 index 0000000..000d3b9 --- /dev/null +++ b/packs_src/resonations/Light_Construct.yaml @@ -0,0 +1,15 @@ +_id: mgne-res-lightconstruct +_key: "!items!mgne-res-lightconstruct" +name: Light Construct +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + A hazy shimmer of coalesced energy stands at your side, ready to defend + you. For D6+1 Rounds, an HP 10, Morale -, Armor D6 creature is summoned. + At the end of each Round, the creature makes an attack with its Thermal + Talons for D6 damage bypassing Armor on a random enemy. If it hits, it + inflicts Burning. Invoking this Resonation increases in difficulty by +2 DR + per construct you currently have summoned. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Mirage.yaml b/packs_src/resonations/Mirage.yaml new file mode 100644 index 0000000..1adfc66 --- /dev/null +++ b/packs_src/resonations/Mirage.yaml @@ -0,0 +1,16 @@ +_id: mgne-res-mirage +_key: "!items!mgne-res-mirage" +name: Mirage +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + You fill your environment with phantom duplicates. For the next D4+1 Rounds + when you are attacked there is a 3-in-4 chance it hits a mirage instead. + After an attack hits one of your mirages, it vanishes and the odds worsen by + a step (first to 2-in-4, then to 1-in-4). This effect ends when you take + damage—and if you are hit while the odds are 3-in-4, the attack deals +2 + damage. Casting this Resonation while under its effects resets the odds to + 3-in-4. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Negate_Injury.yaml b/packs_src/resonations/Negate_Injury.yaml new file mode 100644 index 0000000..04ee977 --- /dev/null +++ b/packs_src/resonations/Negate_Injury.yaml @@ -0,0 +1,14 @@ +_id: mgne-res-negateinjury +_key: "!items!mgne-res-negateinjury" +name: Negate Injury +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + By selecting a different causal branch, you pick a future in which a wound + never occurred. Touched target recovers D6+1 HP and is cured of all + secondary effects from a wound inflicted in the last ten minutes. Your + target may also spend any number of Omens and recover a further D6+3 HP + for each. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Paralyze.yaml b/packs_src/resonations/Paralyze.yaml new file mode 100644 index 0000000..18b5c33 --- /dev/null +++ b/packs_src/resonations/Paralyze.yaml @@ -0,0 +1,11 @@ +_id: mgne-res-paralyze +_key: "!items!mgne-res-paralyze" +name: Paralyze +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + A shiver runs through their bones. They find themselves locked in a rictus. + Target within line of sight is Restrained and suffers +2 DR to breaking free. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Shield.yaml b/packs_src/resonations/Shield.yaml new file mode 100644 index 0000000..90c6a92 --- /dev/null +++ b/packs_src/resonations/Shield.yaml @@ -0,0 +1,13 @@ +_id: mgne-res-shield +_key: "!items!mgne-res-shield" +name: Shield +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + A condensed plank of force wraps around your forearm. The shield lasts for + D4+1 Rounds. During that time, if you are damaged, you may break it to + negate all damage. After breaking your shield, you may not invoke it again + for D2 Rounds. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Shock.yaml b/packs_src/resonations/Shock.yaml new file mode 100644 index 0000000..1356f2e --- /dev/null +++ b/packs_src/resonations/Shock.yaml @@ -0,0 +1,14 @@ +_id: mgne-res-shock +_key: "!items!mgne-res-shock" +name: Shock +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + Thin veins of lightning reach out hungrily towards your prey. Target within + line of sight takes D4 damage bypassing Armor and suffers Stunned (2). You + may then invoke this Resonation a second time, which does not count towards + your invokes per day but does still require a Presence check, and pick a + different target to deal D2 damage bypassing Armor and inflict Stunned (1). + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Shroud.yaml b/packs_src/resonations/Shroud.yaml new file mode 100644 index 0000000..ecd4e78 --- /dev/null +++ b/packs_src/resonations/Shroud.yaml @@ -0,0 +1,19 @@ +_id: mgne-res-shroud +_key: "!items!mgne-res-shroud" +name: Shroud +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + A thin pearlescent membrane covers you, hiding you from view. So long as + you do not move faster than a crawl, do not attack, do not suffer damage, + do not invoke a Resonation, and do not blunder into an environment that + reveals you, you look like a shimmer of heat and enemies must spend an + Action and pass a Presence check (DR 16) to spot you. If you are spotted, + or if you do one of the things listed above, your shroud breaks. While + under your shroud, there are no consequences for failing an attempt to flee. + So long as you do not do anything to break your shroud, it lasts for D10 + minutes—after which you can freely sustain it by spending one of your + remaining invocations per day. + usageDie: d6 + burnedOut: false diff --git a/packs_src/resonations/Summon_Mist.yaml b/packs_src/resonations/Summon_Mist.yaml new file mode 100644 index 0000000..7076259 --- /dev/null +++ b/packs_src/resonations/Summon_Mist.yaml @@ -0,0 +1,15 @@ +_id: mgne-res-summonmist +_key: "!items!mgne-res-summonmist" +name: Summon Mist +type: resonance-core +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/resonation.svg +system: + description: >- + Boiling vapors spill from your palms, enclosing your surroundings in a + dense and oily-smelling fog. For the next D6+1 Rounds, all attacks and + attempts to perceive in the surrounding environment suffer +2 DR. Repeated + invocations do not increase the DR, but do increase the duration of the mist + by D6 Rounds each. On the other hand, fierce gusts of wind reduce its + duration by D4-1 Rounds. + usageDie: d6 + burnedOut: false diff --git a/packs_src/weapons/Club.yaml b/packs_src/weapons/Club.yaml new file mode 100644 index 0000000..033c85a --- /dev/null +++ b/packs_src/weapons/Club.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-club +_key: "!items!mgne-wpn-club" +name: "Club" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d4" + category: "melee" + range: "" + properties: "" + description: "A simple bludgeoning weapon. One-handed." + equipped: false + usageDie: "d6" diff --git a/packs_src/weapons/Dagger.yaml b/packs_src/weapons/Dagger.yaml new file mode 100644 index 0000000..e45bf7c --- /dev/null +++ b/packs_src/weapons/Dagger.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-dagger +_key: "!items!mgne-wpn-dagger" +name: "Dagger" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d4" + category: "melee" + range: "" + properties: "" + description: "A short blade. Can be thrown." + equipped: false + usageDie: "d6" diff --git a/packs_src/weapons/Halberd.yaml b/packs_src/weapons/Halberd.yaml new file mode 100644 index 0000000..ea0eea7 --- /dev/null +++ b/packs_src/weapons/Halberd.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-halberd +_key: "!items!mgne-wpn-halberd" +name: "Halberd" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d10" + category: "melee" + range: "" + properties: "" + description: "A polearm with axe and spike. Two-handed. Reach." + equipped: false + usageDie: "d8" diff --git a/packs_src/weapons/Handaxe.yaml b/packs_src/weapons/Handaxe.yaml new file mode 100644 index 0000000..e80b00d --- /dev/null +++ b/packs_src/weapons/Handaxe.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-handaxe +_key: "!items!mgne-wpn-handaxe" +name: "Handaxe" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d6" + category: "melee" + range: "" + properties: "" + description: "A versatile one-handed axe. Can be thrown." + equipped: false + usageDie: "d6" diff --git a/packs_src/weapons/Heavy_Crossbow_and_Quiver.yaml b/packs_src/weapons/Heavy_Crossbow_and_Quiver.yaml new file mode 100644 index 0000000..868003e --- /dev/null +++ b/packs_src/weapons/Heavy_Crossbow_and_Quiver.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-heavycrossbow +_key: "!items!mgne-wpn-heavycrossbow" +name: "Heavy Crossbow and Quiver" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d8" + category: "ranged" + range: "" + properties: "" + description: "A powerful crossbow. Comes with bolts. Ranged. Slow to reload." + equipped: false + usageDie: "d8" diff --git a/packs_src/weapons/Longsword.yaml b/packs_src/weapons/Longsword.yaml new file mode 100644 index 0000000..67585b4 --- /dev/null +++ b/packs_src/weapons/Longsword.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-longsword +_key: "!items!mgne-wpn-longsword" +name: "Longsword" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d8" + category: "melee" + range: "" + properties: "" + description: "A versatile fighting blade. One or two-handed." + equipped: false + usageDie: "d8" diff --git a/packs_src/weapons/Maul.yaml b/packs_src/weapons/Maul.yaml new file mode 100644 index 0000000..14ead95 --- /dev/null +++ b/packs_src/weapons/Maul.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-maul +_key: "!items!mgne-wpn-maul" +name: "Maul" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d10" + category: "melee" + range: "" + properties: "" + description: "A massive two-handed bludgeon. Two-handed." + equipped: false + usageDie: "d8" diff --git a/packs_src/weapons/Quarterstaff.yaml b/packs_src/weapons/Quarterstaff.yaml new file mode 100644 index 0000000..2575853 --- /dev/null +++ b/packs_src/weapons/Quarterstaff.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-quarterstaff +_key: "!items!mgne-wpn-quarterstaff" +name: "Quarterstaff" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d4" + category: "melee" + range: "" + properties: "" + description: "A long wooden staff. Two-handed." + equipped: false + usageDie: "d8" diff --git a/packs_src/weapons/Rapier.yaml b/packs_src/weapons/Rapier.yaml new file mode 100644 index 0000000..3f02d51 --- /dev/null +++ b/packs_src/weapons/Rapier.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-rapier +_key: "!items!mgne-wpn-rapier" +name: "Rapier" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d6" + category: "melee" + range: "" + properties: "" + description: "A swift and precise thrusting sword." + equipped: false + usageDie: "d8" diff --git a/packs_src/weapons/Shortbow_and_Quiver.yaml b/packs_src/weapons/Shortbow_and_Quiver.yaml new file mode 100644 index 0000000..7e24d6d --- /dev/null +++ b/packs_src/weapons/Shortbow_and_Quiver.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-shortbow +_key: "!items!mgne-wpn-shortbow" +name: "Shortbow and Quiver" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d6" + category: "ranged" + range: "" + properties: "" + description: "A light bow. Comes with a quiver of arrows. Ranged." + equipped: false + usageDie: "d8" diff --git a/packs_src/weapons/Spear.yaml b/packs_src/weapons/Spear.yaml new file mode 100644 index 0000000..fa7fd32 --- /dev/null +++ b/packs_src/weapons/Spear.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-spear +_key: "!items!mgne-wpn-spear" +name: "Spear" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d6" + category: "melee" + range: "" + properties: "" + description: "A thrusting polearm. Can be thrown. One or two-handed." + equipped: false + usageDie: "d8" diff --git a/packs_src/weapons/Whip.yaml b/packs_src/weapons/Whip.yaml new file mode 100644 index 0000000..24d8cf9 --- /dev/null +++ b/packs_src/weapons/Whip.yaml @@ -0,0 +1,13 @@ +_id: mgne-wpn-whip +_key: "!items!mgne-wpn-whip" +name: "Whip" +type: weapon +img: systems/fvtt-machine-gods-noxian-expanse/assets/ui/icons/weapon.svg +system: + damage: "1d4" + category: "melee" + range: "" + properties: "" + description: "A flexible lash with reach." + equipped: false + usageDie: "d8" diff --git a/system.json b/system.json new file mode 100644 index 0000000..a31ec76 --- /dev/null +++ b/system.json @@ -0,0 +1,131 @@ +{ + "id": "fvtt-machine-gods-noxian-expanse", + "title": "Machine Gods of the Noxian Expanse", + "description": "Foundry VTT system for Machine Gods of the Noxian Expanse.", + "version": "14.0.0", + "authors": [ + { + "name": "Uberwald" + } + ], + "compatibility": { + "minimum": "14", + "verified": "14" + }, + "esmodules": [ + "fvtt-machine-gods-noxian-expanse.mjs" + ], + "styles": [ + "css/mgne.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "documentTypes": { + "Actor": { + "character": { + "htmlFields": [ + "description", + "notes" + ] + }, + "creature": { + "htmlFields": [ + "description", + "special", + "notes" + ] + }, + "companion": { + "htmlFields": [ + "description", + "notes" + ] + } + }, + "Item": { + "weapon": { + "htmlFields": [ + "description" + ] + }, + "armor": { + "htmlFields": [ + "description" + ] + }, + "shield": { + "htmlFields": [ + "description" + ] + }, + "equipment": { + "htmlFields": [ + "description" + ] + }, + "resonance-core": { + "htmlFields": [ + "description" + ] + }, + "artifact": { + "htmlFields": [ + "description" + ] + }, + "feature": { + "htmlFields": [ + "description" + ] + } + } + }, + "packs": [ + { + "name": "resonations", + "label": "Resonations", + "path": "packs-system/resonations", + "type": "Item", + "system": "fvtt-machine-gods-noxian-expanse" + }, + { + "name": "features", + "label": "Features", + "path": "packs-system/features", + "type": "Item", + "system": "fvtt-machine-gods-noxian-expanse" + }, + { + "name": "companions", + "label": "Companions", + "path": "packs-system/companions", + "type": "Actor", + "system": "fvtt-machine-gods-noxian-expanse" + }, + { + "name": "weapons", + "label": "Weapons", + "path": "packs-system/weapons", + "type": "Item", + "system": "fvtt-machine-gods-noxian-expanse" + }, + { + "name": "armor", + "label": "Armor & Shields", + "path": "packs-system/armor", + "type": "Item", + "system": "fvtt-machine-gods-noxian-expanse" + } + ], + "grid": { + "distance": 10, + "units": "ft" + }, + "primaryTokenAttribute": "hp", + "socket": false +} diff --git a/templates/armor.hbs b/templates/armor.hbs new file mode 100644 index 0000000..15735d5 --- /dev/null +++ b/templates/armor.hbs @@ -0,0 +1,36 @@ +
+
+ {{item.name}} +
+ +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+

{{localize "MGNE.Common.Description"}}

+
+
+ {{formInput systemFields.description enriched=(lookup enrichedFields "description") value=system.description name="system.description" toggled=true}} +
+
diff --git a/templates/artifact.hbs b/templates/artifact.hbs new file mode 100644 index 0000000..02e6168 --- /dev/null +++ b/templates/artifact.hbs @@ -0,0 +1,40 @@ +
+
+ {{item.name}} +
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+

{{localize "MGNE.Common.Description"}}

+
+
+ {{formInput systemFields.description enriched=(lookup enrichedFields "description") value=system.description name="system.description" toggled=true}} +
+
diff --git a/templates/character-daily.hbs b/templates/character-daily.hbs new file mode 100644 index 0000000..df3a505 --- /dev/null +++ b/templates/character-daily.hbs @@ -0,0 +1,73 @@ +
+
+
+
+ +
+
+ +
+
+ +
+
+ {{localize "MGNE.Common.Current"}} + +
+
+ {{localize "MGNE.Common.Die"}} + +
+
+
+
+ +
+
+ {{localize "MGNE.Common.Used"}} + +
+ / +
+ {{localize "MGNE.Common.Max"}} + +
+
+ {{localize "MGNE.Character.Remaining"}}: {{system.resonance.remaining}} +
+ +
+
+
+ +
+
+ {{localize "MGNE.Common.Used"}} + +
+ / +
+ {{localize "MGNE.Common.Limit"}} + +
+
+
+
+
+
diff --git a/templates/character-equipment.hbs b/templates/character-equipment.hbs new file mode 100644 index 0000000..0289023 --- /dev/null +++ b/templates/character-equipment.hbs @@ -0,0 +1,144 @@ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+
+

{{localize "MGNE.Character.Weapons"}}

+ +
+ {{#each weapons}} +
+
{{name}}
+
{{system.damage}}
+
{{lookup @root.config.weaponCategories system.category}}
+
+ + + + + + +
+
+ {{else}} +

{{localize "MGNE.Empty.NoWeapons"}}

+ {{/each}} +
+ +
+
+

{{localize "MGNE.Character.Armor"}}

+ + +
+ {{#each armors}} +
+
{{name}}
+
{{system.armorDie}}
+
{{localize "MGNE.Common.Penalty"}} {{system.penalty}}
+
+ + + +
+
+ {{/each}} + {{#each shields}} +
+
{{name}}
+
{{system.armorDie}}
+
{{localize "MGNE.ItemTypes.shield"}}
+
+ + + +
+
+ {{/each}} +
+ +
+
+

{{localize "MGNE.ItemTypes.equipment"}}

+ +
+ {{#each equipmentItems}} +
+
{{name}}
+
{{localize "MGNE.Common.QuantityShort"}} {{system.quantity}}
+
{{system.usageDie}}
+
+ + + +
+
+ {{else}} +

{{localize "MGNE.Empty.NoEquipment"}}

+ {{/each}} +
+ +
+
+

{{localize "MGNE.Character.ResonanceCores"}}

+ +
+ {{#each cores}} +
+
{{name}}
+
{{lookup @root.config.resonanceList system.resonationId}}
+
{{system.usageDie}}
+
+ + + + +
+
+ {{else}} +

{{localize "MGNE.Empty.NoResonanceCores"}}

+ {{/each}} +
+ +
+
+

{{localize "MGNE.Character.Artifacts"}}

+ +
+ {{#each artifacts}} +
+
{{name}}
+
{{#if system.synchronized}}{{localize "MGNE.Common.Synchronized"}}{{else}}{{localize "MGNE.Common.Unsynchronized"}}{{/if}}
+
{{system.usageDie}}
+
+ + + + +
+
+ {{else}} +

{{localize "MGNE.Empty.NoArtifacts"}}

+ {{/each}} +
+
diff --git a/templates/character-features.hbs b/templates/character-features.hbs new file mode 100644 index 0000000..af9c7f9 --- /dev/null +++ b/templates/character-features.hbs @@ -0,0 +1,18 @@ +
+
+

{{localize "MGNE.ItemTypes.feature"}}

+ +
+ {{#each features}} +
+
{{name}}
+
{{lookup @root.config.featureChoices system.featureId}}
+
+ + +
+
+ {{else}} +

{{localize "MGNE.Empty.NoFeatures"}}

+ {{/each}} +
diff --git a/templates/character-main.hbs b/templates/character-main.hbs new file mode 100644 index 0000000..1c7f952 --- /dev/null +++ b/templates/character-main.hbs @@ -0,0 +1,53 @@ +
+
+
+ {{actor.name}} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ {{localize "MGNE.Common.Current"}} + +
+ / +
+ {{localize "MGNE.Common.Max"}} + +
+
+
+ + +
+
+
+
+
+
+
diff --git a/templates/character-notes.hbs b/templates/character-notes.hbs new file mode 100644 index 0000000..31a76cd --- /dev/null +++ b/templates/character-notes.hbs @@ -0,0 +1,24 @@ +
+
+
+
+

{{localize "MGNE.Common.Notes"}}

+
+
+ {{formInput systemFields.notes enriched=(lookup enrichedFields "notes") value=system.notes name="system.notes" toggled=true}} +
+
+
+
+

{{localize "MGNE.Character.RulesSnapshot"}}

+
+
+

{{localize "MGNE.RulesSnapshot.Checks"}}

+

{{localize "MGNE.RulesSnapshot.Attacks"}}

+

{{localize "MGNE.RulesSnapshot.Armor"}}

+

{{localize "MGNE.RulesSnapshot.Breaks"}}

+

{{localize "MGNE.RulesSnapshot.Morale"}}

+
+
+
+
diff --git a/templates/character-overview.hbs b/templates/character-overview.hbs new file mode 100644 index 0000000..381cdb5 --- /dev/null +++ b/templates/character-overview.hbs @@ -0,0 +1,63 @@ +
+
+ {{#each abilityList}} +
+ +
+ +
+
+ {{/each}} +
+ +
+ DR 12 + {{localize "MGNE.Abilities.agility"}} +
+
+
+ +
+
+
+

{{localize "MGNE.Common.Description"}}

+
+
+ {{formInput systemFields.description enriched=(lookup enrichedFields "description") value=system.description name="system.description" toggled=true}} +
+
+
+
+

{{localize "MGNE.Common.Conditions"}}

+
+
+ {{#if valueConditions}} +
+ {{#each valueConditions}} + + + {{/each}} +
+ {{/if}} + {{#if flagConditions}} +
+ {{#each flagConditions}} + + {{/each}} +
+ {{/if}} +
+
+
+
diff --git a/templates/character-tabs.hbs b/templates/character-tabs.hbs new file mode 100644 index 0000000..ba44c2d --- /dev/null +++ b/templates/character-tabs.hbs @@ -0,0 +1,5 @@ + diff --git a/templates/chat-message.hbs b/templates/chat-message.hbs new file mode 100644 index 0000000..e70c5f6 --- /dev/null +++ b/templates/chat-message.hbs @@ -0,0 +1,38 @@ +
+
+ {{#if actorImg}}{{actorName}}{{/if}} +
+

{{label}}

+

{{actorName}}

+
+
+
+ {{#if subtitle}}

{{subtitle}}

{{/if}} + {{#if formula}}

{{localize "MGNE.Chat.Formula"}}: {{formula}}

{{/if}} +
+ {{localize "MGNE.Chat.Result"}} + {{total}} +
+

{{outcome}}

+ {{#if specialText}}

{{specialText}}

{{/if}} + {{#if showDamageButton}} +
+ +
+ {{/if}} + {{#if showApplyButton}} +
+ +
+ {{/if}} +
+
diff --git a/templates/companion-main.hbs b/templates/companion-main.hbs new file mode 100644 index 0000000..4f8503d --- /dev/null +++ b/templates/companion-main.hbs @@ -0,0 +1,103 @@ +
+
+ {{actor.name}} +
+ +
+
+ +
+ + / + +
+
+
+ + +
+
+ + +
+
+
+
+ +
+ {{#each abilityList}} +
+ + +
+ {{/each}} +
+ +
+ {{localize "MGNE.Common.Attack"}} +
+
+ + +
+
+ + +
+
+ + + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ {{localize "MGNE.Companion.AdventuringBehavior"}} + {{formInput systemFields.adventuringBehavior enriched=(lookup enrichedFields "adventuringBehavior") value=system.adventuringBehavior name="system.adventuringBehavior" toggled=true}} +
+
+ {{localize "MGNE.Companion.CombatBehavior"}} + {{formInput systemFields.combatBehavior enriched=(lookup enrichedFields "combatBehavior") value=system.combatBehavior name="system.combatBehavior" toggled=true}} +
+
+ +
+
+ {{localize "MGNE.Common.Description"}} + {{formInput systemFields.description enriched=(lookup enrichedFields "description") value=system.description name="system.description" toggled=true}} +
+
+ {{localize "MGNE.Common.Notes"}} + {{formInput systemFields.notes enriched=(lookup enrichedFields "notes") value=system.notes name="system.notes" toggled=true}} +
+
+
diff --git a/templates/creature-main.hbs b/templates/creature-main.hbs new file mode 100644 index 0000000..01dc364 --- /dev/null +++ b/templates/creature-main.hbs @@ -0,0 +1,82 @@ +
+
+ {{actor.name}} +
+ +
+
+ +
+ + / + +
+
+
+ + +
+
+ + +
+
+
+
+ +
+ {{#each abilityList}} +
+ + +
+ {{/each}} +
+ +
+ {{localize "MGNE.Common.Attack"}} +
+
+ + +
+
+ + +
+
+ + + +
+
+
+ +
+
+ {{localize "MGNE.Common.Description"}} + {{formInput systemFields.description enriched=(lookup enrichedFields "description") value=system.description name="system.description" toggled=true}} +
+
+ {{localize "MGNE.Creature.Special"}} + {{formInput systemFields.special enriched=(lookup enrichedFields "special") value=system.special name="system.special" toggled=true}} +
+
+ +
+ {{localize "MGNE.Common.Notes"}} + {{formInput systemFields.notes enriched=(lookup enrichedFields "notes") value=system.notes name="system.notes" toggled=true}} +
+
diff --git a/templates/equipment.hbs b/templates/equipment.hbs new file mode 100644 index 0000000..3999116 --- /dev/null +++ b/templates/equipment.hbs @@ -0,0 +1,41 @@ +
+
+ {{item.name}} +
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+

{{localize "MGNE.Common.Description"}}

+
+
+ {{formInput systemFields.description enriched=(lookup enrichedFields "description") value=system.description name="system.description" toggled=true}} +
+
diff --git a/templates/feature.hbs b/templates/feature.hbs new file mode 100644 index 0000000..71733b6 --- /dev/null +++ b/templates/feature.hbs @@ -0,0 +1,14 @@ +
+
+ {{item.name}} +
+ +
+
+
+

{{localize "MGNE.Common.Description"}}

+
+
+ {{formInput systemFields.description enriched=(lookup enrichedFields "description") value=system.description name="system.description" toggled=true}} +
+
diff --git a/templates/resonance-core.hbs b/templates/resonance-core.hbs new file mode 100644 index 0000000..7baa5cd --- /dev/null +++ b/templates/resonance-core.hbs @@ -0,0 +1,27 @@ +
+
+ {{item.name}} +
+ +
+
+
+
+ + +
+
+
+ +
+
+

{{localize "MGNE.Common.Description"}}

+
+
+ {{formInput systemFields.description enriched=(lookup enrichedFields "description") value=system.description name="system.description" toggled=true}} +
+
diff --git a/templates/roll-dialog.hbs b/templates/roll-dialog.hbs new file mode 100644 index 0000000..da7657f --- /dev/null +++ b/templates/roll-dialog.hbs @@ -0,0 +1,25 @@ +
+

{{label}}

+

{{abilityLabel}} {{localize "MGNE.RollDialog.VsDR"}} {{baseDR}}

+
+
+ + +
+
+ + +
+
+ {{#if omens}} + + {{/if}} +
diff --git a/templates/shield.hbs b/templates/shield.hbs new file mode 100644 index 0000000..15735d5 --- /dev/null +++ b/templates/shield.hbs @@ -0,0 +1,36 @@ +
+
+ {{item.name}} +
+ +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+

{{localize "MGNE.Common.Description"}}

+
+
+ {{formInput systemFields.description enriched=(lookup enrichedFields "description") value=system.description name="system.description" toggled=true}} +
+
diff --git a/templates/weapon.hbs b/templates/weapon.hbs new file mode 100644 index 0000000..6c75483 --- /dev/null +++ b/templates/weapon.hbs @@ -0,0 +1,52 @@ +
+
+ {{item.name}} +
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+

{{localize "MGNE.Common.Description"}}

+
+
+ {{formInput systemFields.description enriched=(lookup enrichedFields "description") value=system.description name="system.description" toggled=true}} +
+
diff --git a/tools/CompendiumsManager.mjs b/tools/CompendiumsManager.mjs new file mode 100644 index 0000000..f556451 --- /dev/null +++ b/tools/CompendiumsManager.mjs @@ -0,0 +1,58 @@ +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-system', 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-system', 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}`; + 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'), + } + ); + } + } + + 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 `${prefix}_${safeFileName}.${yaml ? 'yaml' : 'json'}`; + } +} diff --git a/tools/packCompendiums.mjs b/tools/packCompendiums.mjs new file mode 100644 index 0000000..07052f6 --- /dev/null +++ b/tools/packCompendiums.mjs @@ -0,0 +1,2 @@ +import { CompendiumsManager } from './CompendiumsManager.mjs'; +CompendiumsManager.packToDistDir(); diff --git a/tools/unpackCompendiums.mjs b/tools/unpackCompendiums.mjs new file mode 100644 index 0000000..006013d --- /dev/null +++ b/tools/unpackCompendiums.mjs @@ -0,0 +1,2 @@ +import { CompendiumsManager } from './CompendiumsManager.mjs'; +CompendiumsManager.unpackToSrcDir();