@vttforge/cli 0.0.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +364 -0
- package/LICENSE +21 -0
- package/README.md +33 -5
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +201 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/index.d.mts +496 -0
- package/dist/index.mjs +2 -0
- package/dist/src-CWhUNM__.mjs +2443 -0
- package/dist/src-CWhUNM__.mjs.map +1 -0
- package/package.json +62 -9
- package/templates/module-js/.github/workflows/release.yml +92 -0
- package/templates/module-js/README.md +57 -0
- package/templates/module-js/_gitignore +12 -0
- package/templates/module-js/lang/en.json +11 -0
- package/templates/module-js/module.json +20 -0
- package/templates/module-js/package.json +24 -0
- package/templates/module-js/scripts/main.mjs +65 -0
- package/templates/module-js/styles/main.css +16 -0
- package/templates/module-js/vite.config.mjs +13 -0
- package/templates/module-ts/.github/workflows/release.yml +92 -0
- package/templates/module-ts/README.md +57 -0
- package/templates/module-ts/_gitignore +12 -0
- package/templates/module-ts/lang/en.json +11 -0
- package/templates/module-ts/module.json +20 -0
- package/templates/module-ts/package.json +26 -0
- package/templates/module-ts/scripts/foundry-globals.ts +23 -0
- package/templates/module-ts/scripts/main.ts +71 -0
- package/templates/module-ts/styles/main.css +16 -0
- package/templates/module-ts/tsconfig.json +18 -0
- package/templates/module-ts/vite.config.mjs +13 -0
- package/templates/system-js/.github/workflows/release.yml +92 -0
- package/templates/system-js/README.md +57 -0
- package/templates/system-js/_gitignore +12 -0
- package/templates/system-js/lang/en.json +55 -0
- package/templates/system-js/package.json +25 -0
- package/templates/system-js/scripts/data/character-data.mjs +79 -0
- package/templates/system-js/scripts/data/gear-data.mjs +37 -0
- package/templates/system-js/scripts/main.mjs +79 -0
- package/templates/system-js/scripts/migrations.mjs +43 -0
- package/templates/system-js/scripts/sheets/character-sheet.mjs +164 -0
- package/templates/system-js/scripts/sheets/gear-sheet.mjs +65 -0
- package/templates/system-js/styles/main.css +495 -0
- package/templates/system-js/system.json +44 -0
- package/templates/system-js/template.json +8 -0
- package/templates/system-js/templates/actor/character-sheet.hbs +168 -0
- package/templates/system-js/templates/item/gear-sheet.hbs +54 -0
- package/templates/system-js/vite.config.mjs +16 -0
- package/templates/system-ts/.github/workflows/release.yml +92 -0
- package/templates/system-ts/README.md +57 -0
- package/templates/system-ts/_gitignore +12 -0
- package/templates/system-ts/lang/en.json +55 -0
- package/templates/system-ts/package.json +27 -0
- package/templates/system-ts/scripts/data/character-data.ts +81 -0
- package/templates/system-ts/scripts/data/gear-data.ts +37 -0
- package/templates/system-ts/scripts/foundry-globals.ts +33 -0
- package/templates/system-ts/scripts/main.ts +82 -0
- package/templates/system-ts/scripts/migrations.ts +43 -0
- package/templates/system-ts/scripts/sheets/character-sheet.ts +202 -0
- package/templates/system-ts/scripts/sheets/gear-sheet.ts +68 -0
- package/templates/system-ts/styles/main.css +495 -0
- package/templates/system-ts/system.json +44 -0
- package/templates/system-ts/template.json +8 -0
- package/templates/system-ts/templates/actor/character-sheet.hbs +168 -0
- package/templates/system-ts/templates/item/gear-sheet.hbs +54 -0
- package/templates/system-ts/tsconfig.json +18 -0
- package/templates/system-ts/vite.config.mjs +16 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
release:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Resolve version from tag
|
|
18
|
+
id: version
|
|
19
|
+
env:
|
|
20
|
+
REF: ${{ github.ref_name }}
|
|
21
|
+
run: |
|
|
22
|
+
version="${REF#v}"
|
|
23
|
+
# Git allows surprising characters in tag names. Reject anything
|
|
24
|
+
# that isn't a strict semver-ish identifier so downstream node -e
|
|
25
|
+
# / shell interpolations can't be turned into command injection
|
|
26
|
+
# by a malicious tag push.
|
|
27
|
+
if ! [[ "$version" =~ ^[0-9A-Za-z._+-]+$ ]]; then
|
|
28
|
+
echo "Refusing tag with unsafe characters: $version" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
32
|
+
|
|
33
|
+
- uses: pnpm/action-setup@v4
|
|
34
|
+
with:
|
|
35
|
+
version: 10
|
|
36
|
+
|
|
37
|
+
- uses: actions/setup-node@v4
|
|
38
|
+
with:
|
|
39
|
+
node-version: 22
|
|
40
|
+
|
|
41
|
+
- run: pnpm install
|
|
42
|
+
|
|
43
|
+
- name: Sync manifest version
|
|
44
|
+
env:
|
|
45
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
46
|
+
run: |
|
|
47
|
+
node -e "const fs=require('node:fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version=process.env.VERSION;fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n');"
|
|
48
|
+
|
|
49
|
+
# We invoke vite directly (not `pnpm build` → `vttforge build`) because
|
|
50
|
+
# the release workflow needs to:
|
|
51
|
+
# 1. Run vite build → emit dist/system.json
|
|
52
|
+
# 2. Patch manifest/download URLs into dist/system.json
|
|
53
|
+
# 3. Zip dist/ → release artifact (so the zipped manifest carries URLs)
|
|
54
|
+
# `vttforge build` zips at step 1, which would freeze the un-patched
|
|
55
|
+
# manifest. Eventually the vite-plugin will support env-driven URL
|
|
56
|
+
# injection and this dance disappears.
|
|
57
|
+
- run: pnpm exec vite build
|
|
58
|
+
|
|
59
|
+
- name: Inject release URLs into manifest
|
|
60
|
+
env:
|
|
61
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
62
|
+
REPO: ${{ github.repository }}
|
|
63
|
+
PKG_ID: '{{ID}}'
|
|
64
|
+
run: |
|
|
65
|
+
node -e "const fs=require('node:fs');const path='dist/system.json';const m=JSON.parse(fs.readFileSync(path,'utf8'));const r=process.env.REPO;const v=process.env.VERSION;const id=process.env.PKG_ID;m.manifest=\`https://github.com/\${r}/releases/latest/download/system.json\`;m.download=\`https://github.com/\${r}/releases/download/v\${v}/\${id}-\${v}.zip\`;fs.writeFileSync(path,JSON.stringify(m,null,2)+'\n');"
|
|
66
|
+
|
|
67
|
+
- name: Build release zip
|
|
68
|
+
env:
|
|
69
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
70
|
+
PKG_ID: '{{ID}}'
|
|
71
|
+
run: |
|
|
72
|
+
cd dist
|
|
73
|
+
zip -r "../${PKG_ID}-${VERSION}.zip" .
|
|
74
|
+
|
|
75
|
+
- name: Create GitHub Release
|
|
76
|
+
env:
|
|
77
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
78
|
+
PKG_ID: '{{ID}}'
|
|
79
|
+
uses: softprops/action-gh-release@v2
|
|
80
|
+
with:
|
|
81
|
+
files: |
|
|
82
|
+
${{ env.PKG_ID }}-${{ env.VERSION }}.zip
|
|
83
|
+
dist/system.json
|
|
84
|
+
generate_release_notes: true
|
|
85
|
+
body: |
|
|
86
|
+
## Foundry installation
|
|
87
|
+
|
|
88
|
+
Install via Foundry's setup wizard with this manifest URL:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
https://github.com/${{ github.repository }}/releases/latest/download/system.json
|
|
92
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# {{TITLE}}
|
|
2
|
+
|
|
3
|
+
{{DESCRIPTION}}
|
|
4
|
+
|
|
5
|
+
Built on [VTTForge](https://github.com/vttforge/vttforge) — typed data models, declarative sheet boilerplate, migration runner, error catalogue, and a Vite-based build pipeline.
|
|
6
|
+
|
|
7
|
+
> **Note** — VTTForge is currently in pre-release and not yet published to npm. The dependencies in `package.json` (`@vttforge/core`, `@vttforge/styles`, `@vttforge/vite-plugin`) will resolve once VTTForge ships its first public release. Until then, work from a local checkout: `pnpm link --global` from each VTTForge package, then `pnpm link --global @vttforge/core @vttforge/styles @vttforge/vite-plugin` here.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm install
|
|
13
|
+
pnpm dev # vite build --watch + auto-symlinks dist/ into Foundry's Data
|
|
14
|
+
pnpm build # one-shot build + emits {{ID}}-<version>.zip for foundryvtt.com
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`pnpm dev` (`vttforge dev` under the hood) auto-detects your Foundry user-data
|
|
18
|
+
directory on the first run, prompts you to confirm it, and saves the choice to
|
|
19
|
+
`.vttforge/config.json` for next time. Override with `--data-dir <path>` or set
|
|
20
|
+
`FOUNDRY_DATA_DIR` in your environment.
|
|
21
|
+
|
|
22
|
+
Run Foundry with `--hotReload` so saved files reload the world without a page
|
|
23
|
+
refresh — `dev` watches `dist/` for changes, and Foundry's built-in dispatcher
|
|
24
|
+
swaps CSS / Handlebars / JSON on the fly.
|
|
25
|
+
|
|
26
|
+
Then launch Foundry, create a world that uses **{{TITLE}}**, and open a character — the sheet is the `CharacterSheet` shipped in `scripts/sheets/`.
|
|
27
|
+
|
|
28
|
+
## What's inside
|
|
29
|
+
|
|
30
|
+
| Path | Purpose |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `system.json` | Manifest (id, compatibility, document types, manifest-side sanitization paths) |
|
|
33
|
+
| `template.json` | Foundry document type declarations (Actor + Item subtypes) |
|
|
34
|
+
| `scripts/main.mjs` | Entry point — one call to `registerSystem` from `@vttforge/core` |
|
|
35
|
+
| `scripts/data/*.mjs` | Typed data models (`BaseTypeDataModel()` from `@vttforge/core`) |
|
|
36
|
+
| `scripts/sheets/*.mjs` | Sheet boilerplate eliminators (`BaseActorSheet()` / `BaseItemSheet()`) |
|
|
37
|
+
| `scripts/migrations.mjs` | `createMigrationRunner()` — versioned data migrations |
|
|
38
|
+
| `templates/` | Handlebars templates for sheets |
|
|
39
|
+
| `styles/main.css` | Stylesheet — imports `@vttforge/styles` as the design system base |
|
|
40
|
+
| `lang/en.json` | Localization strings |
|
|
41
|
+
|
|
42
|
+
## Releasing to Foundry
|
|
43
|
+
|
|
44
|
+
Tag-push triggers `.github/workflows/release.yml`, which builds the system, zips `dist/`, and attaches both `{{ID}}-<version>.zip` and `dist/system.json` to a GitHub Release.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git tag v0.1.0
|
|
48
|
+
git push --tags
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Foundry installs from the `system.json` URL on the Release — point users at the `latest/download/system.json` URL in the release notes so they auto-update on every tag.
|
|
52
|
+
|
|
53
|
+
For foundryvtt.com submissions, submit the same manifest URL pattern.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
{{LICENSE}} © {{YEAR}} {{AUTHOR}}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"TYPES.Actor.character": "Character",
|
|
3
|
+
"TYPES.Item.gear": "Gear",
|
|
4
|
+
|
|
5
|
+
"{{LOCALE_PREFIX}}.System.title": "{{TITLE}}",
|
|
6
|
+
"{{LOCALE_PREFIX}}.System.description": "{{DESCRIPTION}}",
|
|
7
|
+
|
|
8
|
+
"{{LOCALE_PREFIX}}.Sheet.Character.title": "Character",
|
|
9
|
+
"{{LOCALE_PREFIX}}.Sheet.Gear.title": "Gear",
|
|
10
|
+
"{{LOCALE_PREFIX}}.Sheet.NamePlaceholder": "Name…",
|
|
11
|
+
"{{LOCALE_PREFIX}}.Sheet.Level": "LV",
|
|
12
|
+
"{{LOCALE_PREFIX}}.Sheet.AbilityScores": "Ability scores",
|
|
13
|
+
"{{LOCALE_PREFIX}}.Sheet.Inventory": "Inventory",
|
|
14
|
+
"{{LOCALE_PREFIX}}.Sheet.Biography": "Biography",
|
|
15
|
+
"{{LOCALE_PREFIX}}.Sheet.NewGear": "New gear",
|
|
16
|
+
"{{LOCALE_PREFIX}}.Sheet.NewGearName": "New Gear",
|
|
17
|
+
"{{LOCALE_PREFIX}}.Sheet.NoGear": "No gear yet — drag items here or click New gear.",
|
|
18
|
+
"{{LOCALE_PREFIX}}.Sheet.Delete": "Delete",
|
|
19
|
+
|
|
20
|
+
"{{LOCALE_PREFIX}}.Sheet.Quick.HP": "HP",
|
|
21
|
+
"{{LOCALE_PREFIX}}.Sheet.Quick.AC": "AC",
|
|
22
|
+
"{{LOCALE_PREFIX}}.Sheet.Quick.SPD": "SPD",
|
|
23
|
+
"{{LOCALE_PREFIX}}.Sheet.Quick.INIT": "INIT",
|
|
24
|
+
|
|
25
|
+
"{{LOCALE_PREFIX}}.Sheet.Tabs.abilities": "Abilities",
|
|
26
|
+
"{{LOCALE_PREFIX}}.Sheet.Tabs.inventory": "Inventory",
|
|
27
|
+
"{{LOCALE_PREFIX}}.Sheet.Tabs.spells": "Spells",
|
|
28
|
+
"{{LOCALE_PREFIX}}.Sheet.Tabs.biography": "Biography",
|
|
29
|
+
"{{LOCALE_PREFIX}}.Sheet.Tabs.details": "Details",
|
|
30
|
+
"{{LOCALE_PREFIX}}.Sheet.Tabs.description": "Description",
|
|
31
|
+
"{{LOCALE_PREFIX}}.Sheet.Spells": "Spells",
|
|
32
|
+
"{{LOCALE_PREFIX}}.Sheet.SpellsEmpty": "Spells slot reserved for a follow-up release.",
|
|
33
|
+
|
|
34
|
+
"{{LOCALE_PREFIX}}.Sheet.Roll.label": "Roll",
|
|
35
|
+
"{{LOCALE_PREFIX}}.Sheet.Roll.flavor": "{ability} check",
|
|
36
|
+
"{{LOCALE_PREFIX}}.Sheet.Drop.label": "Drop item to add",
|
|
37
|
+
"{{LOCALE_PREFIX}}.Sheet.Drop.rejected": "Cannot drop {type} items here — only gear is accepted.",
|
|
38
|
+
|
|
39
|
+
"{{LOCALE_PREFIX}}.Ability.str": "Strength",
|
|
40
|
+
"{{LOCALE_PREFIX}}.Ability.dex": "Dexterity",
|
|
41
|
+
"{{LOCALE_PREFIX}}.Ability.con": "Constitution",
|
|
42
|
+
"{{LOCALE_PREFIX}}.Ability.int": "Intelligence",
|
|
43
|
+
"{{LOCALE_PREFIX}}.Ability.wis": "Wisdom",
|
|
44
|
+
"{{LOCALE_PREFIX}}.Ability.cha": "Charisma",
|
|
45
|
+
|
|
46
|
+
"{{LOCALE_PREFIX}}.Gear.Quantity": "Quantity",
|
|
47
|
+
"{{LOCALE_PREFIX}}.Gear.Weight": "Weight (lb)",
|
|
48
|
+
"{{LOCALE_PREFIX}}.Gear.Kind": "Kind",
|
|
49
|
+
"{{LOCALE_PREFIX}}.Gear.Kind.equipped": "equipped",
|
|
50
|
+
"{{LOCALE_PREFIX}}.Gear.Kind.valued": "valued",
|
|
51
|
+
"{{LOCALE_PREFIX}}.Gear.Kind.stowed": "stowed",
|
|
52
|
+
|
|
53
|
+
"{{LOCALE_PREFIX}}.Settings.showTutorial.name": "Show tutorial",
|
|
54
|
+
"{{LOCALE_PREFIX}}.Settings.showTutorial.hint": "Display the welcome banner on world load."
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{ID}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "{{DESCRIPTION}}",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "{{LICENSE}}",
|
|
8
|
+
"author": "{{AUTHOR}}",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "vttforge build",
|
|
11
|
+
"dev": "vttforge dev"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@vttforge/core": "^0.10.0",
|
|
15
|
+
"@vttforge/styles": "^0.3.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@vttforge/cli": "^0.5.0",
|
|
19
|
+
"@vttforge/vite-plugin": "^0.4.0",
|
|
20
|
+
"vite": "^8.2.2"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=26"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CharacterData — typed schema for the `character` Actor type.
|
|
3
|
+
*
|
|
4
|
+
* Quick stats are HP / AC / SPD / INIT, abilities are the six classic
|
|
5
|
+
* scores (str/dex/con/int/wis/cha). Derived state (modifiers, max HP,
|
|
6
|
+
* armor class, initiative) is computed in-memory in `prepareDerivedData`.
|
|
7
|
+
*
|
|
8
|
+
* `prepareBaseData` initialises fields that Active Effects need to mutate
|
|
9
|
+
* (base max HP before any AE bonus); `prepareDerivedData` then computes
|
|
10
|
+
* the AE-aware values (modifiers, percentages, totals). Never write to the
|
|
11
|
+
* database in either hook — they're purely in-memory derivations.
|
|
12
|
+
*/
|
|
13
|
+
import { BaseTypeDataModel, fields } from '@vttforge/core';
|
|
14
|
+
|
|
15
|
+
export class CharacterData extends BaseTypeDataModel() {
|
|
16
|
+
static defineSchema() {
|
|
17
|
+
const f = fields();
|
|
18
|
+
// The six ability scores are written once, as a factory rather than a
|
|
19
|
+
// shared options object: a field keeps the options it was handed, and
|
|
20
|
+
// some field classes write back into them.
|
|
21
|
+
const score = () =>
|
|
22
|
+
new f.NumberField({
|
|
23
|
+
required: true,
|
|
24
|
+
nullable: false,
|
|
25
|
+
integer: true,
|
|
26
|
+
min: 1,
|
|
27
|
+
max: 30,
|
|
28
|
+
initial: 10,
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
level: new f.NumberField({
|
|
32
|
+
required: true,
|
|
33
|
+
nullable: false,
|
|
34
|
+
integer: true,
|
|
35
|
+
min: 1,
|
|
36
|
+
max: 20,
|
|
37
|
+
initial: 1,
|
|
38
|
+
}),
|
|
39
|
+
speed: new f.NumberField({
|
|
40
|
+
required: true,
|
|
41
|
+
nullable: false,
|
|
42
|
+
integer: true,
|
|
43
|
+
min: 0,
|
|
44
|
+
initial: 30,
|
|
45
|
+
}),
|
|
46
|
+
abilities: new f.SchemaField({
|
|
47
|
+
str: score(),
|
|
48
|
+
dex: score(),
|
|
49
|
+
con: score(),
|
|
50
|
+
int: score(),
|
|
51
|
+
wis: score(),
|
|
52
|
+
cha: score(),
|
|
53
|
+
}),
|
|
54
|
+
health: new f.SchemaField({
|
|
55
|
+
value: new f.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 10 }),
|
|
56
|
+
max: new f.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 10 }),
|
|
57
|
+
}),
|
|
58
|
+
power: new f.SchemaField({
|
|
59
|
+
value: new f.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 5 }),
|
|
60
|
+
max: new f.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 5 }),
|
|
61
|
+
}),
|
|
62
|
+
biography: new f.HTMLField(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
prepareDerivedData() {
|
|
67
|
+
for (const [key, value] of Object.entries(this.abilities)) {
|
|
68
|
+
const score = typeof value === 'number' ? value : (value?.value ?? 10);
|
|
69
|
+
const mod = Math.floor((score - 10) / 2);
|
|
70
|
+
this.abilities[key] = { value: score, mod };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const conMod = this.abilities.con.mod;
|
|
74
|
+
this.health.max = 10 + this.level + conMod;
|
|
75
|
+
|
|
76
|
+
this.armorClass = 10 + this.abilities.dex.mod;
|
|
77
|
+
this.initiative = this.abilities.dex.mod;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GearData — typed schema for the `gear` Item type.
|
|
3
|
+
*
|
|
4
|
+
* `kind` is an enum that drives the inventory pill on the character sheet:
|
|
5
|
+
* `equipped` and `valued` render with the accent pill, `stowed` renders
|
|
6
|
+
* muted.
|
|
7
|
+
*/
|
|
8
|
+
import { BaseTypeDataModel, fields } from '@vttforge/core';
|
|
9
|
+
|
|
10
|
+
const GEAR_KINDS = /** @type {const} */ (['equipped', 'valued', 'stowed']);
|
|
11
|
+
|
|
12
|
+
export class GearData extends BaseTypeDataModel() {
|
|
13
|
+
static defineSchema() {
|
|
14
|
+
const f = fields();
|
|
15
|
+
return {
|
|
16
|
+
quantity: new f.NumberField({
|
|
17
|
+
required: true,
|
|
18
|
+
nullable: false,
|
|
19
|
+
integer: true,
|
|
20
|
+
min: 0,
|
|
21
|
+
initial: 1,
|
|
22
|
+
}),
|
|
23
|
+
weight: new f.NumberField({
|
|
24
|
+
required: true,
|
|
25
|
+
nullable: false,
|
|
26
|
+
min: 0,
|
|
27
|
+
initial: 0,
|
|
28
|
+
}),
|
|
29
|
+
kind: new f.StringField({
|
|
30
|
+
required: true,
|
|
31
|
+
choices: GEAR_KINDS,
|
|
32
|
+
initial: 'stowed',
|
|
33
|
+
}),
|
|
34
|
+
description: new f.HTMLField(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{TITLE}} — entry point.
|
|
3
|
+
*
|
|
4
|
+
* `registerSystem` from `@vttforge/core` replaces the ~60-line
|
|
5
|
+
* `Hooks.once("init", ...)` block most systems copy-paste from each other:
|
|
6
|
+
* data model registration, document class swap, initiative formula, sheet
|
|
7
|
+
* registration, settings, and the `Hooks.once("ready", ...)` migration
|
|
8
|
+
* gate are all wired by one call.
|
|
9
|
+
*/
|
|
10
|
+
import { registerSystem, SystemConfig, VttfError } from '@vttforge/core';
|
|
11
|
+
import { CharacterData } from './data/character-data.mjs';
|
|
12
|
+
import { GearData } from './data/gear-data.mjs';
|
|
13
|
+
import { migrations } from './migrations.mjs';
|
|
14
|
+
import { CharacterSheet } from './sheets/character-sheet.mjs';
|
|
15
|
+
import { GearSheet } from './sheets/gear-sheet.mjs';
|
|
16
|
+
|
|
17
|
+
const SYSTEM_ID = '{{ID}}';
|
|
18
|
+
|
|
19
|
+
const settings = new SystemConfig(SYSTEM_ID);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
registerSystem({
|
|
23
|
+
id: SYSTEM_ID,
|
|
24
|
+
actorDataModels: { character: CharacterData },
|
|
25
|
+
itemDataModels: { gear: GearData },
|
|
26
|
+
combat: {
|
|
27
|
+
initiative: { formula: '1d20 + @abilities.dex.mod', decimals: 2 },
|
|
28
|
+
},
|
|
29
|
+
onAfterInit: () => {
|
|
30
|
+
settings.register('showTutorial', {
|
|
31
|
+
name: '{{LOCALE_PREFIX}}.Settings.showTutorial.name',
|
|
32
|
+
hint: '{{LOCALE_PREFIX}}.Settings.showTutorial.hint',
|
|
33
|
+
scope: 'client',
|
|
34
|
+
config: true,
|
|
35
|
+
type: Boolean,
|
|
36
|
+
default: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Register the schemaVersion setting so migrations.run() has somewhere
|
|
40
|
+
// to read and write the version flag.
|
|
41
|
+
migrations.register();
|
|
42
|
+
|
|
43
|
+
const { Actors, Items } = foundry.documents.collections;
|
|
44
|
+
Actors.unregisterSheet('core', foundry.applications.sheets.ActorSheetV2);
|
|
45
|
+
Actors.registerSheet(SYSTEM_ID, CharacterSheet, {
|
|
46
|
+
types: ['character'],
|
|
47
|
+
makeDefault: true,
|
|
48
|
+
label: '{{LOCALE_PREFIX}}.Sheet.Character.title',
|
|
49
|
+
});
|
|
50
|
+
Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2);
|
|
51
|
+
Items.registerSheet(SYSTEM_ID, GearSheet, {
|
|
52
|
+
types: ['gear'],
|
|
53
|
+
makeDefault: true,
|
|
54
|
+
label: '{{LOCALE_PREFIX}}.Sheet.Gear.title',
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
onReady: async () => {
|
|
58
|
+
if (!game.user?.isGM) return;
|
|
59
|
+
try {
|
|
60
|
+
const ran = await migrations.run();
|
|
61
|
+
if (ran.length > 0) {
|
|
62
|
+
console.info(`[${SYSTEM_ID}] applied migrations:`, ran.join(', '));
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err instanceof VttfError) {
|
|
66
|
+
ui.notifications?.error(`[${err.code}] ${err.message} — see ${err.docsUrl}`, {
|
|
67
|
+
permanent: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err instanceof VttfError) {
|
|
76
|
+
console.error(`[${err.code}] ${err.message} — see ${err.docsUrl}`);
|
|
77
|
+
}
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration registry for {{TITLE}}.
|
|
3
|
+
*
|
|
4
|
+
* Each entry is idempotent — safe to re-run if a prior pass failed
|
|
5
|
+
* mid-sequence. `createMigrationRunner` reads the version flag declared in
|
|
6
|
+
* `system.json` (`flags.{{ID}}.needsMigrationVersion`), runs every
|
|
7
|
+
* migration whose version is newer than what's stored in the world's
|
|
8
|
+
* `schemaVersion` setting, and persists the new high-water mark on success.
|
|
9
|
+
*/
|
|
10
|
+
import { createMigrationRunner } from '@vttforge/core';
|
|
11
|
+
|
|
12
|
+
const SYSTEM_ID = '{{ID}}';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* v0.1.0 — first real schema. Add real migration logic here as soon as the
|
|
16
|
+
* first breaking schema change ships. Until then, this is a no-op example
|
|
17
|
+
* that proves the runner end-to-end.
|
|
18
|
+
*/
|
|
19
|
+
async function migrateToV0_1_0() {
|
|
20
|
+
// Iterate world documents and rewrite legacy fields here. Example:
|
|
21
|
+
//
|
|
22
|
+
// const actors = game.actors?.filter((a) => a.type === 'character') ?? [];
|
|
23
|
+
// for (const actor of actors) {
|
|
24
|
+
// const legacy = actor.system?.bio;
|
|
25
|
+
// if (typeof legacy !== 'string') continue;
|
|
26
|
+
// await actor.update({
|
|
27
|
+
// 'system.biography': legacy,
|
|
28
|
+
// '-=system.bio': null,
|
|
29
|
+
// });
|
|
30
|
+
// }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const migrations = createMigrationRunner({
|
|
34
|
+
systemId: SYSTEM_ID,
|
|
35
|
+
compatibleVersion: '0.0.0',
|
|
36
|
+
migrations: [
|
|
37
|
+
{
|
|
38
|
+
version: '0.1.0',
|
|
39
|
+
description: 'initial schema — placeholder for first breaking change',
|
|
40
|
+
fn: migrateToV0_1_0,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CharacterSheet — tabbed character sheet built on `BaseActorSheet()` from
|
|
3
|
+
* `@vttforge/core`. Demonstrates the full boilerplate-elimination surface:
|
|
4
|
+
*
|
|
5
|
+
* - `static TABS` — `context.tabs.<group>` auto-populated by BaseActorSheet
|
|
6
|
+
* (no manual `_prepareTabs` call).
|
|
7
|
+
* - `static DRAG_DROP` — wires `foundry.applications.ux.DragDrop` on first
|
|
8
|
+
* render with `isEditable`-gated permissions.
|
|
9
|
+
* - Typed `onDropItem(item, event)` — pre-resolves UUIDs, rejects
|
|
10
|
+
* non-`gear` items with a notification, lets `gear` flow through to
|
|
11
|
+
* Foundry's default `_onDropItem` by returning `undefined`.
|
|
12
|
+
*/
|
|
13
|
+
import { BaseActorSheet } from '@vttforge/core';
|
|
14
|
+
|
|
15
|
+
const SYSTEM_ID = '{{ID}}';
|
|
16
|
+
|
|
17
|
+
export class CharacterSheet extends BaseActorSheet() {
|
|
18
|
+
static DEFAULT_OPTIONS = foundry.utils.mergeObject(
|
|
19
|
+
super.DEFAULT_OPTIONS,
|
|
20
|
+
{
|
|
21
|
+
id: '{{ID}}-character',
|
|
22
|
+
classes: ['{{ID}}', 'sheet', 'actor', 'character'],
|
|
23
|
+
window: {
|
|
24
|
+
title: '{{LOCALE_PREFIX}}.Sheet.Character.title',
|
|
25
|
+
icon: 'fa-solid fa-user',
|
|
26
|
+
},
|
|
27
|
+
position: { width: 640, height: 700 },
|
|
28
|
+
actions: {
|
|
29
|
+
rollAbility: CharacterSheet._onRollAbility,
|
|
30
|
+
createGear: CharacterSheet._onCreateGear,
|
|
31
|
+
deleteItem: CharacterSheet._onDeleteItem,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{ inplace: false },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
static PARTS = {
|
|
38
|
+
sheet: {
|
|
39
|
+
template: `systems/${SYSTEM_ID}/templates/actor/character-sheet.hbs`,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
static TABS = {
|
|
44
|
+
primary: {
|
|
45
|
+
tabs: [
|
|
46
|
+
{
|
|
47
|
+
id: 'abilities',
|
|
48
|
+
group: 'primary',
|
|
49
|
+
label: '{{LOCALE_PREFIX}}.Sheet.Tabs.abilities',
|
|
50
|
+
icon: 'fa-solid fa-dumbbell',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'inventory',
|
|
54
|
+
group: 'primary',
|
|
55
|
+
label: '{{LOCALE_PREFIX}}.Sheet.Tabs.inventory',
|
|
56
|
+
icon: 'fa-solid fa-sack',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'spells',
|
|
60
|
+
group: 'primary',
|
|
61
|
+
label: '{{LOCALE_PREFIX}}.Sheet.Tabs.spells',
|
|
62
|
+
icon: 'fa-solid fa-wand-magic-sparkles',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'biography',
|
|
66
|
+
group: 'primary',
|
|
67
|
+
label: '{{LOCALE_PREFIX}}.Sheet.Tabs.biography',
|
|
68
|
+
icon: 'fa-solid fa-feather',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
initial: 'abilities',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
static DRAG_DROP = [
|
|
76
|
+
{
|
|
77
|
+
dragSelector: '.sh-item[draggable=true]',
|
|
78
|
+
dropSelector: '.sh-body',
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/** @override */
|
|
83
|
+
async _prepareContext(options) {
|
|
84
|
+
const context = await super._prepareContext(options);
|
|
85
|
+
const actor = this.document;
|
|
86
|
+
const system = actor.system;
|
|
87
|
+
const abilityLabels = {
|
|
88
|
+
str: '{{LOCALE_PREFIX}}.Ability.str',
|
|
89
|
+
dex: '{{LOCALE_PREFIX}}.Ability.dex',
|
|
90
|
+
con: '{{LOCALE_PREFIX}}.Ability.con',
|
|
91
|
+
int: '{{LOCALE_PREFIX}}.Ability.int',
|
|
92
|
+
wis: '{{LOCALE_PREFIX}}.Ability.wis',
|
|
93
|
+
cha: '{{LOCALE_PREFIX}}.Ability.cha',
|
|
94
|
+
};
|
|
95
|
+
context.actor = actor;
|
|
96
|
+
context.system = system;
|
|
97
|
+
context.isEditable = this.isEditable;
|
|
98
|
+
context.abilities = Object.entries(system.abilities ?? {}).map(([key, data]) => ({
|
|
99
|
+
key,
|
|
100
|
+
label: game.i18n.localize(abilityLabels[key] ?? key),
|
|
101
|
+
value: data?.value ?? data,
|
|
102
|
+
mod: data?.mod ?? 0,
|
|
103
|
+
}));
|
|
104
|
+
context.gear = actor.items
|
|
105
|
+
.filter((item) => item.type === 'gear')
|
|
106
|
+
.map((item) => ({
|
|
107
|
+
id: item.id,
|
|
108
|
+
name: item.name,
|
|
109
|
+
img: item.img,
|
|
110
|
+
kind: item.system?.kind ?? 'stowed',
|
|
111
|
+
quantity: item.system?.quantity ?? 1,
|
|
112
|
+
weight: item.system?.weight ?? 0,
|
|
113
|
+
description: item.system?.description ?? '',
|
|
114
|
+
}));
|
|
115
|
+
context.enrichedBiography = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
|
116
|
+
system.biography ?? '',
|
|
117
|
+
{ relativeTo: actor, secrets: actor.isOwner },
|
|
118
|
+
);
|
|
119
|
+
return context;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Reject non-gear drops with a notification; let gear fall through. */
|
|
123
|
+
async onDropItem(item, _event) {
|
|
124
|
+
if (item?.type !== 'gear') {
|
|
125
|
+
ui.notifications?.warn(
|
|
126
|
+
game.i18n.format('{{LOCALE_PREFIX}}.Sheet.Drop.rejected', { type: item?.type ?? 'unknown' }),
|
|
127
|
+
);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static async _onRollAbility(_event, target) {
|
|
134
|
+
const key = target?.dataset?.ability;
|
|
135
|
+
if (!key) return;
|
|
136
|
+
const actor = this.document;
|
|
137
|
+
const mod = actor.system.abilities?.[key]?.mod ?? 0;
|
|
138
|
+
const roll = new Roll(`1d20 + ${mod}`, actor.getRollData());
|
|
139
|
+
await roll.evaluate();
|
|
140
|
+
await roll.toMessage({
|
|
141
|
+
speaker: ChatMessage.getSpeaker({ actor }),
|
|
142
|
+
flavor: game.i18n.format('{{LOCALE_PREFIX}}.Sheet.Roll.flavor', {
|
|
143
|
+
ability: game.i18n.localize(`{{LOCALE_PREFIX}}.Ability.${key}`),
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static async _onCreateGear(_event, _target) {
|
|
149
|
+
const cls = CONFIG.Item.documentClass;
|
|
150
|
+
const parent = this.document;
|
|
151
|
+
await cls.create(
|
|
152
|
+
{ name: game.i18n.localize('{{LOCALE_PREFIX}}.Sheet.NewGearName'), type: 'gear' },
|
|
153
|
+
{ parent, renderSheet: true },
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static async _onDeleteItem(_event, target) {
|
|
158
|
+
const row = target?.closest('[data-item-id]');
|
|
159
|
+
const id = row?.dataset?.itemId;
|
|
160
|
+
if (!id) return;
|
|
161
|
+
const item = this.document.items.get(id);
|
|
162
|
+
await item?.delete();
|
|
163
|
+
}
|
|
164
|
+
}
|