@vttforge/cli 0.0.0 → 0.5.0

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +350 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -5
  4. package/dist/bin.d.mts +1 -0
  5. package/dist/bin.mjs +201 -0
  6. package/dist/bin.mjs.map +1 -0
  7. package/dist/index.d.mts +496 -0
  8. package/dist/index.mjs +2 -0
  9. package/dist/src-CWhUNM__.mjs +2443 -0
  10. package/dist/src-CWhUNM__.mjs.map +1 -0
  11. package/package.json +62 -9
  12. package/templates/module-js/.github/workflows/release.yml +92 -0
  13. package/templates/module-js/README.md +57 -0
  14. package/templates/module-js/_gitignore +12 -0
  15. package/templates/module-js/lang/en.json +11 -0
  16. package/templates/module-js/module.json +20 -0
  17. package/templates/module-js/package.json +24 -0
  18. package/templates/module-js/scripts/main.mjs +65 -0
  19. package/templates/module-js/styles/main.css +16 -0
  20. package/templates/module-js/vite.config.mjs +13 -0
  21. package/templates/module-ts/.github/workflows/release.yml +92 -0
  22. package/templates/module-ts/README.md +57 -0
  23. package/templates/module-ts/_gitignore +12 -0
  24. package/templates/module-ts/lang/en.json +11 -0
  25. package/templates/module-ts/module.json +20 -0
  26. package/templates/module-ts/package.json +26 -0
  27. package/templates/module-ts/scripts/foundry-globals.ts +23 -0
  28. package/templates/module-ts/scripts/main.ts +71 -0
  29. package/templates/module-ts/styles/main.css +16 -0
  30. package/templates/module-ts/tsconfig.json +18 -0
  31. package/templates/module-ts/vite.config.mjs +13 -0
  32. package/templates/system-js/.github/workflows/release.yml +92 -0
  33. package/templates/system-js/README.md +57 -0
  34. package/templates/system-js/_gitignore +12 -0
  35. package/templates/system-js/lang/en.json +55 -0
  36. package/templates/system-js/package.json +25 -0
  37. package/templates/system-js/scripts/data/character-data.mjs +79 -0
  38. package/templates/system-js/scripts/data/gear-data.mjs +37 -0
  39. package/templates/system-js/scripts/main.mjs +79 -0
  40. package/templates/system-js/scripts/migrations.mjs +43 -0
  41. package/templates/system-js/scripts/sheets/character-sheet.mjs +164 -0
  42. package/templates/system-js/scripts/sheets/gear-sheet.mjs +65 -0
  43. package/templates/system-js/styles/main.css +495 -0
  44. package/templates/system-js/system.json +44 -0
  45. package/templates/system-js/template.json +8 -0
  46. package/templates/system-js/templates/actor/character-sheet.hbs +168 -0
  47. package/templates/system-js/templates/item/gear-sheet.hbs +54 -0
  48. package/templates/system-js/vite.config.mjs +16 -0
  49. package/templates/system-ts/.github/workflows/release.yml +92 -0
  50. package/templates/system-ts/README.md +57 -0
  51. package/templates/system-ts/_gitignore +12 -0
  52. package/templates/system-ts/lang/en.json +55 -0
  53. package/templates/system-ts/package.json +27 -0
  54. package/templates/system-ts/scripts/data/character-data.ts +81 -0
  55. package/templates/system-ts/scripts/data/gear-data.ts +37 -0
  56. package/templates/system-ts/scripts/foundry-globals.ts +33 -0
  57. package/templates/system-ts/scripts/main.ts +82 -0
  58. package/templates/system-ts/scripts/migrations.ts +43 -0
  59. package/templates/system-ts/scripts/sheets/character-sheet.ts +202 -0
  60. package/templates/system-ts/scripts/sheets/gear-sheet.ts +68 -0
  61. package/templates/system-ts/styles/main.css +495 -0
  62. package/templates/system-ts/system.json +44 -0
  63. package/templates/system-ts/template.json +8 -0
  64. package/templates/system-ts/templates/actor/character-sheet.hbs +168 -0
  65. package/templates/system-ts/templates/item/gear-sheet.hbs +54 -0
  66. package/templates/system-ts/tsconfig.json +18 -0
  67. 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,12 @@
1
+ node_modules
2
+ dist
3
+ .turbo
4
+ .vscode
5
+ .DS_Store
6
+ *.log
7
+
8
+ # vttforge dev — saved Foundry data-dir path
9
+ .vttforge/
10
+
11
+ # vttforge build — release artifact
12
+ *.zip
@@ -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.6.0",
15
+ "@vttforge/styles": "^0.3.0"
16
+ },
17
+ "devDependencies": {
18
+ "@vttforge/cli": "^0.5.0",
19
+ "@vttforge/vite-plugin": "^0.3.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
+ }