@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,168 @@
|
|
|
1
|
+
{{!--
|
|
2
|
+
Character sheet — markup mirrors the canonical Figma reference
|
|
3
|
+
(character-sheet.jsx + Theme Explorations .sh-* classes).
|
|
4
|
+
|
|
5
|
+
Tabs nav uses `data-tab`/`data-group` + `data-action="vttforgeTab"` so
|
|
6
|
+
BaseActorSheet's default tab action routes clicks. `context.tabs.primary`
|
|
7
|
+
is auto-populated by BaseActorSheet's _prepareContext.
|
|
8
|
+
|
|
9
|
+
`<img data-edit="img">` is handled by DocumentSheetV2's built-in editImage
|
|
10
|
+
action. `<prose-mirror>` is the v13 rich text element for biography.
|
|
11
|
+
--}}
|
|
12
|
+
<form autocomplete="off">
|
|
13
|
+
|
|
14
|
+
{{!-- ════════ HEAD (.sh-head) ════════ --}}
|
|
15
|
+
<div class="sh-head">
|
|
16
|
+
<img class="sh-portrait" src="{{actor.img}}" alt="{{actor.name}}" data-edit="img" />
|
|
17
|
+
<div class="sh-id">
|
|
18
|
+
<div class="sh-id-top">
|
|
19
|
+
<input class="sh-name" type="text" name="name" value="{{actor.name}}"
|
|
20
|
+
placeholder="{{localize '{{LOCALE_PREFIX}}.Sheet.NamePlaceholder'}}" />
|
|
21
|
+
<div class="sh-sub">
|
|
22
|
+
<span>{{localize "{{LOCALE_PREFIX}}.Sheet.Level"}}</span>
|
|
23
|
+
<input type="number" name="system.level" value="{{system.level}}" min="1" max="20" />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="sh-quick">
|
|
27
|
+
<div class="q">
|
|
28
|
+
<div class="qv">
|
|
29
|
+
<input type="number" name="system.health.value" value="{{system.health.value}}" min="0" />
|
|
30
|
+
<span class="qv-sep">/</span>
|
|
31
|
+
<span class="qv-max">{{system.health.max}}</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="qk">{{localize "{{LOCALE_PREFIX}}.Sheet.Quick.HP"}}</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="q">
|
|
36
|
+
<div class="qv">{{system.armorClass}}</div>
|
|
37
|
+
<div class="qk">{{localize "{{LOCALE_PREFIX}}.Sheet.Quick.AC"}}</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="q">
|
|
40
|
+
<div class="qv">
|
|
41
|
+
<input type="number" name="system.speed" value="{{system.speed}}" min="0" />
|
|
42
|
+
</div>
|
|
43
|
+
<div class="qk">{{localize "{{LOCALE_PREFIX}}.Sheet.Quick.SPD"}}</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="q">
|
|
46
|
+
<div class="qv">{{numberFormat system.initiative sign=true}}</div>
|
|
47
|
+
<div class="qk">{{localize "{{LOCALE_PREFIX}}.Sheet.Quick.INIT"}}</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{{!-- ════════ TABS (.sh-tabs) ════════ --}}
|
|
54
|
+
<nav class="sh-tabs" data-group="primary">
|
|
55
|
+
{{#each tabs as |tab|}}
|
|
56
|
+
<button type="button" class="sh-tab {{#if tab.active}}on{{/if}}"
|
|
57
|
+
data-tab="{{tab.id}}"
|
|
58
|
+
data-group="primary"
|
|
59
|
+
data-action="vttforgeTab"
|
|
60
|
+
title="{{localize tab.label}}">
|
|
61
|
+
{{#if tab.icon}}<i class="{{tab.icon}}"></i>{{/if}}
|
|
62
|
+
<span>{{localize tab.label}}</span>
|
|
63
|
+
</button>
|
|
64
|
+
{{/each}}
|
|
65
|
+
</nav>
|
|
66
|
+
|
|
67
|
+
{{!-- ════════ BODY (.sh-body) ════════ --}}
|
|
68
|
+
<section class="sh-body">
|
|
69
|
+
|
|
70
|
+
{{!-- ABILITIES --}}
|
|
71
|
+
<section class="tab abilities {{tabs.abilities.cssClass}}"
|
|
72
|
+
data-tab="abilities" data-group="primary">
|
|
73
|
+
<h3 class="sh-section-title">{{localize "{{LOCALE_PREFIX}}.Sheet.AbilityScores"}}</h3>
|
|
74
|
+
<div class="sh-abilities">
|
|
75
|
+
{{#each abilities as |ability|}}
|
|
76
|
+
<div class="sh-abil">
|
|
77
|
+
<div class="lbl" title="{{ability.label}}">{{ability.key}}</div>
|
|
78
|
+
<input class="val" type="number"
|
|
79
|
+
name="system.abilities.{{ability.key}}"
|
|
80
|
+
value="{{ability.value}}" min="1" max="30" />
|
|
81
|
+
<button type="button" class="mod sh-abil-roll"
|
|
82
|
+
data-action="rollAbility" data-ability="{{ability.key}}"
|
|
83
|
+
title="{{localize '{{LOCALE_PREFIX}}.Sheet.Roll.label'}}">
|
|
84
|
+
{{numberFormat ability.mod sign=true}}
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
{{/each}}
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
{{!-- INVENTORY --}}
|
|
92
|
+
<section class="tab inventory {{tabs.inventory.cssClass}}"
|
|
93
|
+
data-tab="inventory" data-group="primary">
|
|
94
|
+
<header class="sh-section-head">
|
|
95
|
+
<h3 class="sh-section-title">{{localize "{{LOCALE_PREFIX}}.Sheet.Inventory"}}</h3>
|
|
96
|
+
<button type="button" class="sh-action" data-action="createGear"
|
|
97
|
+
title="{{localize '{{LOCALE_PREFIX}}.Sheet.NewGear'}}">
|
|
98
|
+
<i class="fa-solid fa-plus"></i>
|
|
99
|
+
<span>{{localize "{{LOCALE_PREFIX}}.Sheet.NewGear"}}</span>
|
|
100
|
+
</button>
|
|
101
|
+
</header>
|
|
102
|
+
<div class="sh-items">
|
|
103
|
+
{{#each gear as |item|}}
|
|
104
|
+
<div class="sh-item" data-item-id="{{item.id}}" draggable="true">
|
|
105
|
+
<div class="sh-item-ico">
|
|
106
|
+
{{#if item.img}}
|
|
107
|
+
<img src="{{item.img}}" alt="{{item.name}}" />
|
|
108
|
+
{{else}}
|
|
109
|
+
<i class="fa-solid fa-box"></i>
|
|
110
|
+
{{/if}}
|
|
111
|
+
</div>
|
|
112
|
+
<div>
|
|
113
|
+
<div class="sh-item-name">{{item.name}}</div>
|
|
114
|
+
<div class="sh-item-meta">{{numberFormat item.weight}} lb</div>
|
|
115
|
+
</div>
|
|
116
|
+
<span class="sh-tag {{#if (eq item.kind "stowed")}}muted{{/if}}">
|
|
117
|
+
{{localize (concat "{{LOCALE_PREFIX}}.Gear.Kind." item.kind)}}
|
|
118
|
+
</span>
|
|
119
|
+
<span class="sh-qty">×{{item.quantity}}</span>
|
|
120
|
+
<button type="button" class="sh-item-delete" data-action="deleteItem"
|
|
121
|
+
title="{{localize '{{LOCALE_PREFIX}}.Sheet.Delete'}}">
|
|
122
|
+
<i class="fa-solid fa-trash"></i>
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
{{else}}
|
|
126
|
+
<div class="sh-items-empty">{{localize "{{LOCALE_PREFIX}}.Sheet.NoGear"}}</div>
|
|
127
|
+
{{/each}}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="sh-drop">
|
|
131
|
+
<div class="lbl">{{localize "{{LOCALE_PREFIX}}.Sheet.Drop.label"}}</div>
|
|
132
|
+
<div class="sub">onDropItem(item, event)</div>
|
|
133
|
+
</div>
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
{{!-- SPELLS (placeholder) --}}
|
|
137
|
+
<section class="tab spells {{tabs.spells.cssClass}}"
|
|
138
|
+
data-tab="spells" data-group="primary">
|
|
139
|
+
<h3 class="sh-section-title">{{localize "{{LOCALE_PREFIX}}.Sheet.Spells"}}</h3>
|
|
140
|
+
<div class="sh-empty">{{localize "{{LOCALE_PREFIX}}.Sheet.SpellsEmpty"}}</div>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
{{!-- BIOGRAPHY --}}
|
|
144
|
+
<section class="tab biography {{tabs.biography.cssClass}}"
|
|
145
|
+
data-tab="biography" data-group="primary">
|
|
146
|
+
<h3 class="sh-section-title">{{localize "{{LOCALE_PREFIX}}.Sheet.Biography"}}</h3>
|
|
147
|
+
{{#if isEditable}}
|
|
148
|
+
<prose-mirror name="system.biography"
|
|
149
|
+
button="true"
|
|
150
|
+
editable="{{isEditable}}"
|
|
151
|
+
toggled="false"
|
|
152
|
+
value="{{system.biography}}">
|
|
153
|
+
{{{enrichedBiography}}}
|
|
154
|
+
</prose-mirror>
|
|
155
|
+
{{else}}
|
|
156
|
+
<div class="sh-biography-readonly">{{{enrichedBiography}}}</div>
|
|
157
|
+
{{/if}}
|
|
158
|
+
</section>
|
|
159
|
+
|
|
160
|
+
</section>
|
|
161
|
+
|
|
162
|
+
{{!-- ════════ FOOT (.sh-foot) ════════ --}}
|
|
163
|
+
<footer class="sh-foot">
|
|
164
|
+
<span class="nm">VTTFORGE · FORGE</span>
|
|
165
|
+
<span>@vttforge/styles · v0.2</span>
|
|
166
|
+
</footer>
|
|
167
|
+
|
|
168
|
+
</form>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{{!-- Gear sheet — minimal demo of BaseItemSheet on ItemSheetV2. --}}
|
|
2
|
+
<form autocomplete="off">
|
|
3
|
+
|
|
4
|
+
<header class="sheet-header">
|
|
5
|
+
<img class="portrait" src="{{item.img}}" alt="{{item.name}}" data-edit="img" />
|
|
6
|
+
<input type="text" name="name" value="{{item.name}}"
|
|
7
|
+
placeholder="{{localize '{{LOCALE_PREFIX}}.Sheet.NamePlaceholder'}}" />
|
|
8
|
+
</header>
|
|
9
|
+
|
|
10
|
+
<nav class="sheet-tabs" data-group="primary">
|
|
11
|
+
{{#each tabs as |tab|}}
|
|
12
|
+
<button type="button" class="item {{tab.cssClass}}"
|
|
13
|
+
data-tab="{{tab.id}}"
|
|
14
|
+
data-group="primary"
|
|
15
|
+
data-action="vttforgeTab"
|
|
16
|
+
title="{{localize tab.label}}">
|
|
17
|
+
{{#if tab.icon}}<i class="{{tab.icon}}"></i>{{/if}}
|
|
18
|
+
<span>{{localize tab.label}}</span>
|
|
19
|
+
</button>
|
|
20
|
+
{{/each}}
|
|
21
|
+
</nav>
|
|
22
|
+
|
|
23
|
+
<section class="sheet-body">
|
|
24
|
+
{{!-- DETAILS --}}
|
|
25
|
+
<section class="tab details {{tabs.details.cssClass}}"
|
|
26
|
+
data-tab="details" data-group="primary">
|
|
27
|
+
<div class="field">
|
|
28
|
+
<label>{{localize "{{LOCALE_PREFIX}}.Gear.Quantity"}}</label>
|
|
29
|
+
<input type="number" name="system.quantity" value="{{system.quantity}}" min="0" />
|
|
30
|
+
</div>
|
|
31
|
+
<div class="field">
|
|
32
|
+
<label>{{localize "{{LOCALE_PREFIX}}.Gear.Weight"}}</label>
|
|
33
|
+
<input type="number" name="system.weight" value="{{system.weight}}" min="0" step="0.1" />
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
|
|
37
|
+
{{!-- DESCRIPTION --}}
|
|
38
|
+
<section class="tab description {{tabs.description.cssClass}}"
|
|
39
|
+
data-tab="description" data-group="primary">
|
|
40
|
+
{{#if isEditable}}
|
|
41
|
+
<prose-mirror name="system.description"
|
|
42
|
+
button="true"
|
|
43
|
+
editable="{{isEditable}}"
|
|
44
|
+
toggled="false"
|
|
45
|
+
value="{{system.description}}">
|
|
46
|
+
{{{enrichedDescription}}}
|
|
47
|
+
</prose-mirror>
|
|
48
|
+
{{else}}
|
|
49
|
+
<div class="description-readonly">{{{enrichedDescription}}}</div>
|
|
50
|
+
{{/if}}
|
|
51
|
+
</section>
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
</form>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import vttforge from '@vttforge/vite-plugin';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Vite config for {{TITLE}}.
|
|
6
|
+
*
|
|
7
|
+
* `@vttforge/vite-plugin` owns the build contract — it bundles
|
|
8
|
+
* `scripts/main.mjs` into `dist/main.mjs` (no hash, no bare specifiers),
|
|
9
|
+
* resolves `@import "@vttforge/styles"` at build time, copies the manifest +
|
|
10
|
+
* `lang/` + `templates/` to `dist/`, and syncs the manifest version from
|
|
11
|
+
* `package.json` on every build. The result under `dist/` is the deployable
|
|
12
|
+
* artefact you symlink into Foundry's `Data/systems/{{ID}}/`.
|
|
13
|
+
*/
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
plugins: [vttforge({ id: '{{ID}}' })],
|
|
16
|
+
});
|
|
@@ -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.ts` | Entry point — one call to `registerSystem` from `@vttforge/core` |
|
|
35
|
+
| `scripts/data/*.ts` | Typed data models (`BaseTypeDataModel()` from `@vttforge/core`) |
|
|
36
|
+
| `scripts/sheets/*.ts` | Sheet boilerplate eliminators (`BaseActorSheet()` / `BaseItemSheet()`) |
|
|
37
|
+
| `scripts/migrations.ts` | `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,27 @@
|
|
|
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
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@vttforge/core": "^0.10.0",
|
|
16
|
+
"@vttforge/styles": "^0.3.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@vttforge/cli": "^0.5.0",
|
|
20
|
+
"@vttforge/vite-plugin": "^0.4.0",
|
|
21
|
+
"typescript": "^5.4.0",
|
|
22
|
+
"vite": "^8.2.2"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=26"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
// biome-ignore lint/suspicious/noExplicitAny: schema-driven types ship in a later @vttforge/types release
|
|
68
|
+
const self = this as any;
|
|
69
|
+
for (const [key, value] of Object.entries(self.abilities)) {
|
|
70
|
+
const score = typeof value === 'number' ? value : ((value as { value?: number })?.value ?? 10);
|
|
71
|
+
const mod = Math.floor((score - 10) / 2);
|
|
72
|
+
self.abilities[key] = { value: score, mod };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const conMod = self.abilities.con.mod;
|
|
76
|
+
self.health.max = 10 + self.level + conMod;
|
|
77
|
+
|
|
78
|
+
self.armorClass = 10 + self.abilities.dex.mod;
|
|
79
|
+
self.initiative = self.abilities.dex.mod;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -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 = ['equipped', 'valued', 'stowed'] as const;
|
|
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 as unknown as string[],
|
|
32
|
+
initial: 'stowed',
|
|
33
|
+
}),
|
|
34
|
+
description: new f.HTMLField(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Foundry runtime globals.
|
|
3
|
+
*
|
|
4
|
+
* These declarations let the scaffold compile cleanly without an external
|
|
5
|
+
* type package. Replace with `fvtt-types` (community-maintained, pinned to
|
|
6
|
+
* a known-working commit SHA) or `@vttforge/types` (coming in a later
|
|
7
|
+
* VTTForge release) for full type coverage:
|
|
8
|
+
*
|
|
9
|
+
* pnpm add -D 'github:League-of-Foundry-Developers/foundry-vtt-types#main'
|
|
10
|
+
*
|
|
11
|
+
* Then declare `types: ["fvtt-types"]` in `tsconfig.json` and delete this
|
|
12
|
+
* file.
|
|
13
|
+
*/
|
|
14
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: stub declarations only
|
|
18
|
+
const game: any;
|
|
19
|
+
// biome-ignore lint/suspicious/noExplicitAny: stub declarations only
|
|
20
|
+
const CONFIG: any;
|
|
21
|
+
// biome-ignore lint/suspicious/noExplicitAny: stub declarations only
|
|
22
|
+
const Hooks: any;
|
|
23
|
+
// biome-ignore lint/suspicious/noExplicitAny: stub declarations only
|
|
24
|
+
const ui: any;
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: stub declarations only
|
|
26
|
+
const ChatMessage: any;
|
|
27
|
+
// biome-ignore lint/suspicious/noExplicitAny: stub declarations only
|
|
28
|
+
const Roll: any;
|
|
29
|
+
// biome-ignore lint/suspicious/noExplicitAny: stub declarations only
|
|
30
|
+
const foundry: any;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {};
|