From 89d722ddf3b1aa2d74488c225b4c558e14aba8ae Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:59:11 -0700 Subject: [PATCH 01/46] copy from `progress-model-dirty` --- src/module/actor/subtypes/site.ts | 67 ++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/src/module/actor/subtypes/site.ts b/src/module/actor/subtypes/site.ts index 1f6737c20..925c73e77 100644 --- a/src/module/actor/subtypes/site.ts +++ b/src/module/actor/subtypes/site.ts @@ -1,19 +1,27 @@ import type { TableResultDataConstructorData } from '@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/data/data.mjs/tableResultData' -import type { ChallengeRank } from 'dataforged' -import { ChallengeRankField } from '../../fields/ChallengeRankField' -import { ProgressTicksField } from '../../fields/ProgressTicksField' import type { TableResultStub } from '../../fields/TableResultField' import { TableResultField } from '../../fields/TableResultField' import type { DataSchema } from '../../fields/utils' +import type { + ProgressTrackPropertiesData, + ProgressTrackSource +} from '../../model/progress-track' +import { ProgressTrack } from '../../model/progress-track' import { OracleTable } from '../../roll-table/oracle-table' import type { IronswornActor } from '../actor' +import type { IronActorModel } from './common' -export class SiteData extends foundry.abstract.TypeDataModel< - SiteDataSourceData, - SiteDataSourceData, - IronswornActor<'site'> -> { - static _enableV10Validation = true +export class SiteData + extends foundry.abstract.TypeDataModel< + SiteDataSourceData, + SiteDataPropertiesData, + IronswornActor<'site'> + > + implements IronActorModel +{ + isValidImpact(statusEffect: StatusEffectV11): boolean { + return false + } get denizenTable() { return new OracleTable({ @@ -111,11 +119,28 @@ export class SiteData extends foundry.abstract.TypeDataModel< return this.theme != null && this.domain != null } - static override defineSchema(): DataSchema { + async markProgress(times = 1) { + return await this.parent.update({ + system: { track: this.track.getMarkData(times) } + }) + } + + prepareDerivedData(): void { + super.prepareDerivedData() + + this.track.value = this.track.score + this.track.max = ProgressTrack.SCORE_MAX + } + + static override defineSchema(): DataSchema< + SiteDataSourceData, + SiteDataPropertiesData + > { const fields = foundry.data.fields return { - rank: new ChallengeRankField(), - current: new ProgressTicksField(), + track: new fields.EmbeddedDataField(ProgressTrack, { + initial: { enabled: true } + }) as any, objective: new fields.HTMLField(), description: new fields.HTMLField(), notes: new fields.HTMLField(), @@ -173,17 +198,29 @@ export class SiteData extends foundry.abstract.TypeDataModel< }) } } + + static migrateData(source) { + const migrate = foundry.abstract.Document._addDataFieldMigration + migrate(source, 'rank', 'track.rank') + migrate(source, 'current', 'track.ticks') + + return source + } } -export interface SiteData extends SiteDataSourceData {} +export interface SiteData extends SiteDataPropertiesData { + track: ProgressTrack +} interface SiteDataSourceData { objective: string description: string notes: string - rank: ChallengeRank - current: number denizens: TableResultStub[] + track: ProgressTrackSource +} +interface SiteDataPropertiesData extends Omit { + track: ProgressTrackPropertiesData } export interface SiteDataSource { From eaa4acc616df51e3a547c49662ca74b68960ca4f Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:00:30 -0700 Subject: [PATCH 02/46] copy from `progress-model-dirty` --- src/module/item/subtypes/progress.ts | 117 ++++++++------------------- 1 file changed, 34 insertions(+), 83 deletions(-) diff --git a/src/module/item/subtypes/progress.ts b/src/module/item/subtypes/progress.ts index 78b79e0d5..6af48812f 100644 --- a/src/module/item/subtypes/progress.ts +++ b/src/module/item/subtypes/progress.ts @@ -1,111 +1,62 @@ -import { clamp } from 'lodash-es' -import { RANK_INCREMENTS } from '../../constants' -import { ChallengeRankField } from '../../fields/ChallengeRankField' -import { ProgressTicksField } from '../../fields/ProgressTicksField' import type { DataSchema } from '../../fields/utils' -import { localizeRank } from '../../helpers/util' -import { IronswornPrerollDialog } from '../../rolls' import type { IronswornItem } from '../item' -import type { ProgressBase } from '../config' +import type { ProgressTrackSource } from '../../model/progress-track' +import { ProgressTrack } from '../../model/progress-track' +import type { ClockSource } from '../../model/clock' +import { Clock } from '../../model/clock' export class ProgressData extends foundry.abstract.TypeDataModel< ProgressDataSourceData, - ProgressDataSourceData, + ProgressDataPropertiesData, IronswornItem<'progress'> > { - static _enableV10Validation = true - - static readonly SCORE_MIN = 0 - static readonly SCORE_MAX = 10 - static readonly TICKS_PER_BOX = 4 - static readonly BOXES = this.SCORE_MAX - static readonly TICKS_MIN = 0 - static readonly TICKS_MAX = this.TICKS_PER_BOX * this.BOXES - - /** The derived progress score, which is an integer from 0 to 10. */ - get score() { - return Math.min( - Math.floor(this.current / ProgressData.TICKS_PER_BOX), - ProgressData.SCORE_MAX - ) - } - - /** The number of ticks per unit of progress (in other words, per instance of "mark progress") for this track's challenge rank. */ - get unit() { - return RANK_INCREMENTS[this.rank] - } - - /** Mark progress on this track. Use negative `units` to erase progress. - * @param units The number of units of progress to be marked (default: `1`). - */ - async markProgress(units = 1) { + async markProgress(times = 1) { return await this.parent.update({ - 'system.current': clamp( - this.current + this.unit * units, - ProgressData.TICKS_MIN, - ProgressData.TICKS_MAX - ) + system: { track: this.track.getMarkData(times) } }) } - async fulfill() { - let moveDfId: string | undefined - if (this.subtype === 'vow') { - const toolset = this.parent.actor?.toolset ?? 'starforged' - moveDfId = - toolset === 'starforged' - ? 'Starforged/Moves/Quest/Fulfill_Your_Vow' - : 'Ironsworn/Moves/Quest/Fulfill_Your_Vow' + static override migrateData(source) { + const migrate = foundry.abstract.Document._addDataFieldMigration + if (source.hasClock === true) { + migrate(source, 'hasClock', 'clock.enabled') + migrate(source, 'clockTicks', 'clock.value') + migrate(source, 'clockMax', 'clock.max') } - return await IronswornPrerollDialog.showForProgress( - this.parent.name ?? '(progress)', - this.score, - this.parent.actor ?? undefined, - moveDfId - ) - } + migrate(source, 'subtype', 'track.subtype') + migrate(source, 'starred', 'flags.foundry-ironsworn.starred') + migrate(source, 'hasTrack', 'track.enabled') + migrate(source, 'rank', 'track.rank') + migrate(source, 'current', 'track.ticks') - /** Provide a localized label for this progress track's challenge rank. */ - localizeRank() { - return localizeRank(this.rank) + return source } - static override defineSchema(): DataSchema { + static override defineSchema(): DataSchema { const fields = foundry.data.fields return { - subtype: new fields.StringField({ initial: 'progress' }), - starred: new fields.BooleanField({ initial: false }), - hasTrack: new fields.BooleanField({ initial: true }), - hasClock: new foundry.data.fields.BooleanField(), - clockTicks: new foundry.data.fields.NumberField({ - initial: 0, - integer: true, - min: 0, - max: 12 + track: new fields.EmbeddedDataField(ProgressTrack, { + initial: { enabled: true } }), - clockMax: new foundry.data.fields.NumberField({ - initial: 4, - choices: [4, 6, 8, 10, 12] - }), - completed: new fields.BooleanField({ initial: false }), - current: new ProgressTicksField(), - description: new fields.HTMLField(), - rank: new ChallengeRankField() + clock: new fields.EmbeddedDataField(Clock), + completed: new fields.BooleanField({ required: false }), + description: new fields.HTMLField() } } } export interface ProgressData extends ProgressDataPropertiesData {} -export interface ProgressDataSourceData extends ProgressBase { - subtype: string - starred: boolean - hasTrack: boolean - hasClock: boolean - clockTicks: number - clockMax: number +export interface ProgressDataSourceData { + description: string + track: ProgressTrackSource + completed?: boolean + clock?: ClockSource +} +export interface ProgressDataPropertiesData extends ProgressDataSourceData { + track: ProgressTrack + clock?: Clock } -export interface ProgressDataPropertiesData extends ProgressDataSourceData {} export interface ProgressDataSource { type: 'progress' From 8069d9b3154fbad72e1fe8e75875c6bbf0cdd54a Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:01:52 -0700 Subject: [PATCH 03/46] copy from `progress-model-dirty` --- src/module/model/clock.ts | 96 +++++++++++++ src/module/model/progress-track.ts | 223 +++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+) create mode 100644 src/module/model/clock.ts create mode 100644 src/module/model/progress-track.ts diff --git a/src/module/model/clock.ts b/src/module/model/clock.ts new file mode 100644 index 000000000..059b71eb1 --- /dev/null +++ b/src/module/model/clock.ts @@ -0,0 +1,96 @@ +import type { ConfiguredDocumentClassForName } from '@league-of-foundry-developers/foundry-vtt-types/src/types/helperTypes' +import type { DocumentType } from '../../types/helperTypes' +import type { DataSchema } from '../fields/utils' + +export class Clock< + Parent extends foundry.abstract.DataModel.AnyOrDoc = foundry.abstract.DataModel.AnyOrDoc +> extends foundry.abstract.DataModel { + static readonly MIN = 0 + + async advance(times = 1) { + const valueField = this.schema.getField( + 'value' + ) as unknown as foundry.data.fields.NumberField + const value = Math.clamped(this.value + times, Clock.MIN, this.max) + return await this.#getNearestDocument()?.update({ + [valueField.fieldPath]: value + }) + } + + async set(value: number) { + const valueField = this.schema.getField( + 'value' + ) as unknown as foundry.data.fields.NumberField + return await this.#getNearestDocument()?.update({ + [valueField.fieldPath]: Math.clamped(value, Clock.MIN, this.max) + }) + } + + /** Get the most recent Document ancestor */ + #getNearestDocument(): Parent extends foundry.abstract.Document + ? Parent + : foundry.abstract.Document | null + #getNearestDocument( + type: T + ): Parent extends InstanceType> + ? Parent + : InstanceType> | null + #getNearestDocument( + type?: T + ): T extends DocumentType + ? Parent extends InstanceType> + ? Parent + : InstanceType> | null + : foundry.abstract.Document | null { + let DocClass: typeof foundry.abstract.Document + + if (type == null) DocClass = foundry.abstract.Document + else + DocClass = getDocumentClass(type) as ConfiguredDocumentClassForName< + Exclude + > + + if (DocClass == null) return null as any + + if (this.parent instanceof DocClass) return this.parent as any + + let current: foundry.abstract.DataModel.AnyOrDoc | null | undefined = + this.parent + + while (current != null) + if (current.parent instanceof DocClass) return current.parent as any + else current = current.parent + + return null as any + } + + static override defineSchema(): DataSchema { + return { + value: new foundry.data.fields.NumberField({ + initial: 0, + integer: true, + min: 0, + max: 12, + label: 'IRONSWORN.SegmentsFilled' + }), + max: new foundry.data.fields.NumberField({ + initial: 4, + integer: true, + choices: [4, 6, 8, 10, 12], + min: 4, + max: 12, + label: 'IRONSWORN.SegmentsMax' + }), + enabled: new foundry.data.fields.BooleanField({ required: false }) + } + } +} +export interface Clock< + Parent extends foundry.abstract.DataModel.AnyOrDoc = foundry.abstract.DataModel.AnyOrDoc +> extends ClockSource {} + +export interface ClockSource { + value: number + max: 4 | 6 | 8 | 10 | 12 + enabled?: boolean +} diff --git a/src/module/model/progress-track.ts b/src/module/model/progress-track.ts new file mode 100644 index 000000000..3504d6d66 --- /dev/null +++ b/src/module/model/progress-track.ts @@ -0,0 +1,223 @@ +import type { ConfiguredDocumentClassForName } from '@league-of-foundry-developers/foundry-vtt-types/src/types/helperTypes' +import type { ChallengeRank } from 'dataforged' +import { IRONSWORN } from '../../config' +import type { DocumentType } from '../../types/helperTypes' +import { ChallengeRankField } from '../fields/ChallengeRankField' +import type { DataSchema } from '../fields/utils' +import type { IronswornItem } from '../item/item' +import { IronswornPrerollDialog } from '../rolls' + +export class ProgressTrack< + Parent extends foundry.abstract.DataModel.AnyOrDoc = foundry.abstract.DataModel.AnyOrDoc +> extends foundry.abstract.DataModel< + ProgressTrackSource, + ProgressTrackPropertiesData, + Parent +> { + /** Get the most recent Document ancestor */ + getNearestDocument(): Parent extends foundry.abstract.Document + ? Parent + : foundry.abstract.Document | null + getNearestDocument( + type: T + ): Parent extends InstanceType> + ? Parent + : InstanceType> | null + getNearestDocument( + type?: T + ): T extends DocumentType + ? Parent extends InstanceType> + ? Parent + : InstanceType> | null + : foundry.abstract.Document | null { + let DocClass: typeof foundry.abstract.Document + + if (type == null) DocClass = foundry.abstract.Document + else + DocClass = getDocumentClass(type) as ConfiguredDocumentClassForName< + Exclude + > + + if (DocClass == null) return null as any + + if (this.parent instanceof DocClass) return this.parent as any + + let current: foundry.abstract.DataModel.AnyOrDoc | null | undefined = + this.parent + + while (current != null) + if (current.parent instanceof DocClass) return current.parent as any + else current = current.parent + + return null as any + } + + max?: number + value?: number + + /** The derived progress score, an integer from 0 to 10. */ + get score() { + return Math.clamped( + Math.floor(this.ticks / ProgressTrack.TICKS_PER_BOX), + ProgressTrack.SCORE_MIN, + ProgressTrack.SCORE_MAX + ) + } + + /** The number of ticks per unit of progress (in other words, per instance of "mark progress") for this track's challenge rank. */ + get #unit() { + return ProgressTrack.INCREMENT[this.rank] + } + + /** + * Configure a `Document.update` data object for use in marking this progress track. Use negative `times` to erase progress. + * @param times The number of units of progress to be marked (default: `1`). + */ + getMarkData(times = 1) { + return { + ticks: Math.clamped( + this.ticks + this.#unit * times, + ProgressTrack.TICKS_MIN, + ProgressTrack.TICKS_MAX + ) + } + } + + #inferObjective(): string | null { + if (typeof (this.parent as any)?.objective === 'string') + return (this.parent as any).objective + const docTypes: DocumentType[] = ['Item', 'JournalEntryPage'] + for (const docType of docTypes) { + const doc = this.getNearestDocument(docType) as any + if (doc != null) return doc.name ?? null + } + return null + } + + /** Make a progress roll to resolve the progress track. */ + async resolve(objective?: string) { + let moveDfId: string | undefined + const actor = this.getNearestDocument('Actor') + const toolset = actor?.toolset ?? 'starforged' + + switch (this.subtype) { + case 'vow': + moveDfId = + toolset === 'starforged' + ? 'Starforged/Moves/Quest/Fulfill_Your_Vow' + : 'Ironsworn/Moves/Quest/Fulfill_Your_Vow' + break + case 'connection': + if (toolset === 'starforged') + moveDfId = 'Starforged/Moves/Connection/Forge_a_Bond' + break + default: + break + } + + return await IronswornPrerollDialog.showForProgress( + objective ?? this.#inferObjective() ?? '(progress)', + this.score, + actor ?? undefined, + moveDfId + ) + } + + static override migrateData(source) { + // @ts-expect-error + source = super.migrateData(source) + foundry.abstract.Document._addDataFieldMigration(source, 'current', 'ticks') + + if (source.subtype === 'bond') source.subtype = 'connection' + + return source + } + + /** Provide a localized label for this progress track's challenge rank. */ + localizeRank() { + const field = this.schema.getField('rank') as unknown as ChallengeRankField + return game.i18n.localize(field.choices[this.rank]) + } + + static override defineSchema(): DataSchema { + const fields = foundry.data.fields + return { + ticks: new fields.NumberField({ + initial: this.TICKS_MIN, + min: this.TICKS_MIN, + max: this.TICKS_MAX + }), + enabled: new fields.BooleanField({ initial: true }), + rank: new ChallengeRankField(), + progress_move: new fields.ForeignDocumentField( + IRONSWORN.IronswornItem as any, + { + required: false, + nullable: true + } + ) as any, + subtype: new fields.StringField({ + choices: { + progress: 'IRONSWORN.ITEM.SubtypeProgress', + vow: 'IRONSWORN.ITEM.SubtypeVow', + connection: 'IRONSWORN.ITEM.SubtypeConnection', + foe: 'IRONSWORN.ITEM.SubtypeFoe' + }, + initial: 'progress' + }) + } + } + + /** The minimum score when making a progress roll. */ + static readonly SCORE_MIN = 0 + /** The maximum score when making a progress roll. */ + static readonly SCORE_MAX = 10 + /** The number of ticks in one box of progress. */ + static readonly TICKS_PER_BOX = 4 + /** The number of boxes in a progress track. */ + static readonly BOXES = this.SCORE_MAX + /** The minimum number of ticks in a progress track. */ + static readonly TICKS_MIN = 0 + /** The maximum number of ticks in a progress track. */ + static readonly TICKS_MAX = this.TICKS_PER_BOX * this.BOXES + + static readonly INCREMENT: Record< + | ValueOf<(typeof ChallengeRankField)['RANK']> + | keyof (typeof ChallengeRankField)['RANK'], + number + > = { + [ChallengeRankField.RANK.Troublesome]: 12, + Troublesome: 12, + [ChallengeRankField.RANK.Dangerous]: 8, + Dangerous: 8, + [ChallengeRankField.RANK.Formidable]: 4, + Formidable: 4, + [ChallengeRankField.RANK.Extreme]: 2, + Extreme: 2, + [ChallengeRankField.RANK.Epic]: 1, + Epic: 1 + } as const +} +export interface ProgressTrack< + Parent extends foundry.abstract.DataModel.AnyOrDoc +> extends foundry.abstract.DataModel< + ProgressTrackSource, + ProgressTrackPropertiesData, + Parent + >, + ProgressTrackPropertiesData {} + +type ProgressSubtype = 'vow' | 'progress' | 'connection' | 'foe' + +export interface ProgressTrackSource { + rank: ChallengeRank + ticks: number // previously: current + subtype: ProgressSubtype + enabled?: boolean + progress_move?: string | null +} + +export interface ProgressTrackPropertiesData + extends Omit { + progress_move: IronswornItem<'sfmove'> | null +} From 88c1953e98fd01d8bec901f2d5d16c2d163b51df Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:04:34 -0700 Subject: [PATCH 04/46] rm some unused methods --- src/module/model/clock.ts | 64 +++------------------------------------ 1 file changed, 4 insertions(+), 60 deletions(-) diff --git a/src/module/model/clock.ts b/src/module/model/clock.ts index 059b71eb1..4bf9763dc 100644 --- a/src/module/model/clock.ts +++ b/src/module/model/clock.ts @@ -1,5 +1,3 @@ -import type { ConfiguredDocumentClassForName } from '@league-of-foundry-developers/foundry-vtt-types/src/types/helperTypes' -import type { DocumentType } from '../../types/helperTypes' import type { DataSchema } from '../fields/utils' export class Clock< @@ -7,63 +5,6 @@ export class Clock< > extends foundry.abstract.DataModel { static readonly MIN = 0 - async advance(times = 1) { - const valueField = this.schema.getField( - 'value' - ) as unknown as foundry.data.fields.NumberField - const value = Math.clamped(this.value + times, Clock.MIN, this.max) - return await this.#getNearestDocument()?.update({ - [valueField.fieldPath]: value - }) - } - - async set(value: number) { - const valueField = this.schema.getField( - 'value' - ) as unknown as foundry.data.fields.NumberField - return await this.#getNearestDocument()?.update({ - [valueField.fieldPath]: Math.clamped(value, Clock.MIN, this.max) - }) - } - - /** Get the most recent Document ancestor */ - #getNearestDocument(): Parent extends foundry.abstract.Document - ? Parent - : foundry.abstract.Document | null - #getNearestDocument( - type: T - ): Parent extends InstanceType> - ? Parent - : InstanceType> | null - #getNearestDocument( - type?: T - ): T extends DocumentType - ? Parent extends InstanceType> - ? Parent - : InstanceType> | null - : foundry.abstract.Document | null { - let DocClass: typeof foundry.abstract.Document - - if (type == null) DocClass = foundry.abstract.Document - else - DocClass = getDocumentClass(type) as ConfiguredDocumentClassForName< - Exclude - > - - if (DocClass == null) return null as any - - if (this.parent instanceof DocClass) return this.parent as any - - let current: foundry.abstract.DataModel.AnyOrDoc | null | undefined = - this.parent - - while (current != null) - if (current.parent instanceof DocClass) return current.parent as any - else current = current.parent - - return null as any - } - static override defineSchema(): DataSchema { return { value: new foundry.data.fields.NumberField({ @@ -81,7 +22,10 @@ export class Clock< max: 12, label: 'IRONSWORN.SegmentsMax' }), - enabled: new foundry.data.fields.BooleanField({ required: false }) + enabled: new foundry.data.fields.BooleanField({ + required: false, + label: 'IRONSWORN.Enabled' + }) } } } From 9e03c1472d5d7ccba1931285f1419ef77d39d94d Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:11:45 -0700 Subject: [PATCH 05/46] clean up imports + references --- src/module/fields/ChallengeRankField.ts | 48 +++++++++++++------------ 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/module/fields/ChallengeRankField.ts b/src/module/fields/ChallengeRankField.ts index 957ef910a..025172915 100644 --- a/src/module/fields/ChallengeRankField.ts +++ b/src/module/fields/ChallengeRankField.ts @@ -1,8 +1,16 @@ -import { ChallengeRank } from '../constants' -import { enumEntries } from '../fields/utils' +export class ChallengeRankField extends foundry.data.fields.NumberField { + /** + * Enumerates challenge ranks. + * @enum + */ + static readonly RANK = { + Troublesome: 1, + Dangerous: 2, + Formidable: 3, + Extreme: 4, + Epic: 5 + } as const -export class ChallengeRankField extends foundry.data.fields - .NumberField { constructor( options?: Partial< Omit< @@ -14,30 +22,28 @@ export class ChallengeRankField extends foundry.data.fields super({ label: 'IRONSWORN.ChallengeRank', choices: Object.fromEntries( - enumEntries(ChallengeRank).map(([k, v]) => [ - v, - `IRONSWORN.CHALLENGERANK.${k}` + Object.entries(ChallengeRankField.RANK).map(([key, numericValue]) => [ + numericValue, + `IRONSWORN.CHALLENGERANK.${key}` ]) - ) as any, - initial: ChallengeRank.Troublesome, + ) as { + [R in keyof typeof ChallengeRankField.RANK as (typeof ChallengeRankField)['RANK'][R]]: string + }, + initial: ChallengeRankField.RANK.Troublesome as number, integer: true, - max: ChallengeRank.Epic, - min: ChallengeRank.Troublesome, + min: ChallengeRankField.RANK.Troublesome, + max: ChallengeRankField.RANK.Epic, ...options }) } - override _cast(value) { + override _cast(value: unknown) { switch (true) { - // migration: "formidible" -> "formidable" - // TODO: use this instead of migration #1 case value === 'formidible': - return ChallengeRank.Formidable - // migration: string-based challenge ranks to numeric ones - // TODO: use this instead of migration #5 + return ChallengeRankField.RANK.Formidable case typeof value === 'string': - return ChallengeRank[ - (value as string).capitalize() as keyof ChallengeRank + return ChallengeRankField.RANK[ + (value as string).capitalize() as keyof typeof ChallengeRankField.RANK ] default: { return super._cast(value) @@ -45,6 +51,4 @@ export class ChallengeRankField extends foundry.data.fields } } } - -export interface ChallengeRankField - extends foundry.data.fields.NumberField {} +export interface ChallengeRankField extends foundry.data.fields.NumberField {} From 501a0e79d836bfd3690c4f4b38a2540c5e80bc46 Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:18:41 -0700 Subject: [PATCH 06/46] add item class to IRONSWORN. clean up imports --- src/config.ts | 3 ++ src/module/fields/ChallengeRankField.ts | 6 ++- src/module/model/progress-track.ts | 61 ++----------------------- 3 files changed, 13 insertions(+), 57 deletions(-) diff --git a/src/config.ts b/src/config.ts index cd4dc268d..eb53cf95c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,6 +20,7 @@ import { registerOracleTree } from './module/features/customoracles' import { OracleTable } from './module/roll-table/oracle-table' +import { IronswornItem } from './module/item/item' export interface EmitterEvents extends Record { highlightMove: string // Foundry UUID @@ -33,6 +34,7 @@ export type IronswornEmitter = Emitter export interface IronswornConfig { actorClass: typeof IronswornActor OracleTable: typeof OracleTable + IronswornItem: typeof IronswornItem applications: { // Dialogs @@ -65,6 +67,7 @@ export interface IronswornConfig { export const IRONSWORN: IronswornConfig = { actorClass: IronswornActor, OracleTable, + IronswornItem, applications: { FirstStartDialog, diff --git a/src/module/fields/ChallengeRankField.ts b/src/module/fields/ChallengeRankField.ts index 025172915..334220884 100644 --- a/src/module/fields/ChallengeRankField.ts +++ b/src/module/fields/ChallengeRankField.ts @@ -51,4 +51,8 @@ export class ChallengeRankField extends foundry.data.fields.NumberField { } } } -export interface ChallengeRankField extends foundry.data.fields.NumberField {} +export interface ChallengeRankField extends foundry.data.fields.NumberField { + choices: { + [R in keyof typeof ChallengeRankField.RANK as (typeof ChallengeRankField)['RANK'][R]]: string + } +} diff --git a/src/module/model/progress-track.ts b/src/module/model/progress-track.ts index 3504d6d66..7049d6bbe 100644 --- a/src/module/model/progress-track.ts +++ b/src/module/model/progress-track.ts @@ -1,7 +1,6 @@ -import type { ConfiguredDocumentClassForName } from '@league-of-foundry-developers/foundry-vtt-types/src/types/helperTypes' import type { ChallengeRank } from 'dataforged' import { IRONSWORN } from '../../config' -import type { DocumentType } from '../../types/helperTypes' +import type { IronswornActor } from '../actor/actor' import { ChallengeRankField } from '../fields/ChallengeRankField' import type { DataSchema } from '../fields/utils' import type { IronswornItem } from '../item/item' @@ -14,44 +13,6 @@ export class ProgressTrack< ProgressTrackPropertiesData, Parent > { - /** Get the most recent Document ancestor */ - getNearestDocument(): Parent extends foundry.abstract.Document - ? Parent - : foundry.abstract.Document | null - getNearestDocument( - type: T - ): Parent extends InstanceType> - ? Parent - : InstanceType> | null - getNearestDocument( - type?: T - ): T extends DocumentType - ? Parent extends InstanceType> - ? Parent - : InstanceType> | null - : foundry.abstract.Document | null { - let DocClass: typeof foundry.abstract.Document - - if (type == null) DocClass = foundry.abstract.Document - else - DocClass = getDocumentClass(type) as ConfiguredDocumentClassForName< - Exclude - > - - if (DocClass == null) return null as any - - if (this.parent instanceof DocClass) return this.parent as any - - let current: foundry.abstract.DataModel.AnyOrDoc | null | undefined = - this.parent - - while (current != null) - if (current.parent instanceof DocClass) return current.parent as any - else current = current.parent - - return null as any - } - max?: number value?: number @@ -83,21 +44,9 @@ export class ProgressTrack< } } - #inferObjective(): string | null { - if (typeof (this.parent as any)?.objective === 'string') - return (this.parent as any).objective - const docTypes: DocumentType[] = ['Item', 'JournalEntryPage'] - for (const docType of docTypes) { - const doc = this.getNearestDocument(docType) as any - if (doc != null) return doc.name ?? null - } - return null - } - /** Make a progress roll to resolve the progress track. */ - async resolve(objective?: string) { + async resolve(actor?: IronswornActor, objective?: string) { let moveDfId: string | undefined - const actor = this.getNearestDocument('Actor') const toolset = actor?.toolset ?? 'starforged' switch (this.subtype) { @@ -116,7 +65,7 @@ export class ProgressTrack< } return await IronswornPrerollDialog.showForProgress( - objective ?? this.#inferObjective() ?? '(progress)', + objective ?? '(progress)', this.score, actor ?? undefined, moveDfId @@ -124,7 +73,6 @@ export class ProgressTrack< } static override migrateData(source) { - // @ts-expect-error source = super.migrateData(source) foundry.abstract.Document._addDataFieldMigration(source, 'current', 'ticks') @@ -149,13 +97,14 @@ export class ProgressTrack< }), enabled: new fields.BooleanField({ initial: true }), rank: new ChallengeRankField(), + // @ts-expect-error progress_move: new fields.ForeignDocumentField( IRONSWORN.IronswornItem as any, { required: false, nullable: true } - ) as any, + ), subtype: new fields.StringField({ choices: { progress: 'IRONSWORN.ITEM.SubtypeProgress', From 3b7e1b172c23a3d0eb3800bfcf2fbbee30065f92 Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:25:10 -0700 Subject: [PATCH 07/46] some toolset detection logic --- src/module/model/progress-track.ts | 34 ++++++++++-------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/module/model/progress-track.ts b/src/module/model/progress-track.ts index 7049d6bbe..67d9c7c8b 100644 --- a/src/module/model/progress-track.ts +++ b/src/module/model/progress-track.ts @@ -1,10 +1,9 @@ import type { ChallengeRank } from 'dataforged' -import { IRONSWORN } from '../../config' import type { IronswornActor } from '../actor/actor' import { ChallengeRankField } from '../fields/ChallengeRankField' import type { DataSchema } from '../fields/utils' -import type { IronswornItem } from '../item/item' import { IronswornPrerollDialog } from '../rolls' +import { IronswornSettings } from '../helpers/settings' export class ProgressTrack< Parent extends foundry.abstract.DataModel.AnyOrDoc = foundry.abstract.DataModel.AnyOrDoc @@ -47,18 +46,18 @@ export class ProgressTrack< /** Make a progress roll to resolve the progress track. */ async resolve(actor?: IronswornActor, objective?: string) { let moveDfId: string | undefined - const toolset = actor?.toolset ?? 'starforged' + const isStarforged = + actor?.toolset === 'starforged' ?? + IronswornSettings.starforgedToolsEnabled switch (this.subtype) { case 'vow': - moveDfId = - toolset === 'starforged' - ? 'Starforged/Moves/Quest/Fulfill_Your_Vow' - : 'Ironsworn/Moves/Quest/Fulfill_Your_Vow' + moveDfId = isStarforged + ? 'Starforged/Moves/Quest/Fulfill_Your_Vow' + : 'Ironsworn/Moves/Quest/Fulfill_Your_Vow' break case 'connection': - if (toolset === 'starforged') - moveDfId = 'Starforged/Moves/Connection/Forge_a_Bond' + if (isStarforged) moveDfId = 'Starforged/Moves/Connection/Forge_a_Bond' break default: break @@ -73,6 +72,7 @@ export class ProgressTrack< } static override migrateData(source) { + // @ts-expect-error source = super.migrateData(source) foundry.abstract.Document._addDataFieldMigration(source, 'current', 'ticks') @@ -97,14 +97,6 @@ export class ProgressTrack< }), enabled: new fields.BooleanField({ initial: true }), rank: new ChallengeRankField(), - // @ts-expect-error - progress_move: new fields.ForeignDocumentField( - IRONSWORN.IronswornItem as any, - { - required: false, - nullable: true - } - ), subtype: new fields.StringField({ choices: { progress: 'IRONSWORN.ITEM.SubtypeProgress', @@ -160,13 +152,9 @@ type ProgressSubtype = 'vow' | 'progress' | 'connection' | 'foe' export interface ProgressTrackSource { rank: ChallengeRank - ticks: number // previously: current + ticks: number subtype: ProgressSubtype enabled?: boolean - progress_move?: string | null } -export interface ProgressTrackPropertiesData - extends Omit { - progress_move: IronswornItem<'sfmove'> | null -} +export interface ProgressTrackPropertiesData extends ProgressTrackSource {} From 769448b87db2825660e77b9d5c33d1c512ce7261 Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:28:19 -0700 Subject: [PATCH 08/46] provide statics on class as constants --- src/module/model/clock.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/module/model/clock.ts b/src/module/model/clock.ts index 4bf9763dc..b8f454d8a 100644 --- a/src/module/model/clock.ts +++ b/src/module/model/clock.ts @@ -4,22 +4,25 @@ export class Clock< Parent extends foundry.abstract.DataModel.AnyOrDoc = foundry.abstract.DataModel.AnyOrDoc > extends foundry.abstract.DataModel { static readonly MIN = 0 + static readonly MAX = 12 + static readonly SIZES = [4, 6, 8, 10, 12] + static readonly SIZE_MIN = 4 static override defineSchema(): DataSchema { return { value: new foundry.data.fields.NumberField({ - initial: 0, + initial: Clock.MIN, integer: true, - min: 0, - max: 12, + min: Clock.MIN, + max: Clock.MAX, label: 'IRONSWORN.SegmentsFilled' }), max: new foundry.data.fields.NumberField({ - initial: 4, + initial: Clock.SIZE_MIN, integer: true, - choices: [4, 6, 8, 10, 12], - min: 4, - max: 12, + choices: Clock.SIZES as any[], + min: Clock.SIZE_MIN, + max: Clock.MAX, label: 'IRONSWORN.SegmentsMax' }), enabled: new foundry.data.fields.BooleanField({ From 16f8988c482b475d170abbba3ec8a7e168a76683 Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:30:35 -0700 Subject: [PATCH 09/46] as const for stricter typing --- src/module/model/clock.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/module/model/clock.ts b/src/module/model/clock.ts index b8f454d8a..ad10606ba 100644 --- a/src/module/model/clock.ts +++ b/src/module/model/clock.ts @@ -3,10 +3,10 @@ import type { DataSchema } from '../fields/utils' export class Clock< Parent extends foundry.abstract.DataModel.AnyOrDoc = foundry.abstract.DataModel.AnyOrDoc > extends foundry.abstract.DataModel { - static readonly MIN = 0 - static readonly MAX = 12 - static readonly SIZES = [4, 6, 8, 10, 12] - static readonly SIZE_MIN = 4 + static readonly MIN = 0 as const + static readonly MAX = 12 as const + static readonly SIZES = [4, 6, 8, 10, 12] as const + static readonly SIZE_MIN = 4 as const static override defineSchema(): DataSchema { return { @@ -20,7 +20,7 @@ export class Clock< max: new foundry.data.fields.NumberField({ initial: Clock.SIZE_MIN, integer: true, - choices: Clock.SIZES as any[], + choices: Clock.SIZES as any, min: Clock.SIZE_MIN, max: Clock.MAX, label: 'IRONSWORN.SegmentsMax' From a6bb047d509ab371d4e3f5052923c084bccc3ae3 Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:36:30 -0700 Subject: [PATCH 10/46] copy from `progress-model-dirty` --- .../progress/progress-list-item.vue | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/module/vue/components/progress/progress-list-item.vue b/src/module/vue/components/progress/progress-list-item.vue index 2a61fe494..803fc1e91 100644 --- a/src/module/vue/components/progress/progress-list-item.vue +++ b/src/module/vue/components/progress/progress-list-item.vue @@ -4,23 +4,23 @@
{{ subtitle }}
- + @click="$item?.system.markProgress()" /> + @@ -116,8 +116,7 @@ const editMode = computed(() => { return (actor?.value.flags as any)['foundry-ironsworn']?.['edit-mode'] }) const subtitle = computed(() => { - let subtype = props.item.system.subtype.capitalize() - if (subtype === 'Bond') subtype = 'Connection' // translate name + const subtype = props.item.system.track.subtype.capitalize() return game.i18n.localize(`IRONSWORN.ITEM.Subtype${subtype}`) }) const completedIcon = computed(() => { @@ -151,9 +150,11 @@ function toggleComplete() { $item?.update({ system: { completed } }) } function toggleStar() { - $item?.update({ - system: { starred: !props.item.system.starred } - }) + $item?.setFlag( + 'foundry-ironsworn', + 'starred', + !props.item.flags['foundry-ironsworn']?.starred + ) } From e6dc23345552124362ec2dfa8f66c65fd222e1cd Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:37:54 -0700 Subject: [PATCH 11/46] copy form `progress-model-dirty` --- .../vue/components/progress/progress-item-detail.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/module/vue/components/progress/progress-item-detail.vue b/src/module/vue/components/progress/progress-item-detail.vue index 77db083d5..4d7f8ac72 100644 --- a/src/module/vue/components/progress/progress-item-detail.vue +++ b/src/module/vue/components/progress/progress-item-detail.vue @@ -2,11 +2,11 @@

- {{ $item.system.localizeRank() }} + {{ $item.system.track.localizeRank() }}

+ @click="$item?.system.track.getMarkData()" />
From ce0a3b2348a5c8b75658b6bbc2a042db23c755c1 Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:39:52 -0700 Subject: [PATCH 12/46] update jsdoc --- .../vue/components/progress/active-completed-progresses.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/module/vue/components/progress/active-completed-progresses.vue b/src/module/vue/components/progress/active-completed-progresses.vue index cb5e1318c..96a41377f 100644 --- a/src/module/vue/components/progress/active-completed-progresses.vue +++ b/src/module/vue/components/progress/active-completed-progresses.vue @@ -25,7 +25,7 @@ import CompletedProgressList from 'component:progress/completed-progress-list.vu defineProps<{ /** * List of progress subtypes to exclude from the list. To leave out - * connections, pass `['bond']` here. + * connections, pass `['connection']` here. */ excludedSubtypes?: string[] progressStars?: boolean From 12d72c6ea9625a2c13f790ca41bd901549bcd789 Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:41:32 -0700 Subject: [PATCH 13/46] copy from `progress-model-dirty` --- src/module/vue/components/progress/progress-common.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/module/vue/components/progress/progress-common.ts b/src/module/vue/components/progress/progress-common.ts index 616fc9fdd..88f42dca4 100644 --- a/src/module/vue/components/progress/progress-common.ts +++ b/src/module/vue/components/progress/progress-common.ts @@ -2,10 +2,10 @@ import type { IronswornActor } from '../../../actor/actor' import type { ProgressDataPropertiesData } from '../../../item/subtypes/progress' export type CompletedProgressType = 'completed-only' | 'no-completed' | 'all' -export type ProgressSubtype = ProgressDataPropertiesData['subtype'] +export type ProgressSubtype = ProgressDataPropertiesData['track']['subtype'] export function isValidProgressItem( - item: any, + item: ItemSource<'progress'>, showCompleted: CompletedProgressType, excludedSubtypes?: ProgressSubtype[] ) { @@ -26,7 +26,7 @@ export function isValidProgressItem( default: break } - if ((excludedSubtypes ?? []).includes(item.system.subtype)) { + if ((excludedSubtypes ?? []).includes(item.system.track.subtype)) { return false } return true From 4236d78fd5861f387903f22b7ac2ef150a50470a Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:44:26 -0700 Subject: [PATCH 14/46] copy from `progress-model-dirty` --- src/module/vue/components/progress/progress-list.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/module/vue/components/progress/progress-list.vue b/src/module/vue/components/progress/progress-list.vue index 1b9aac35f..81d2f2919 100644 --- a/src/module/vue/components/progress/progress-list.vue +++ b/src/module/vue/components/progress/progress-list.vue @@ -32,7 +32,7 @@ import { getProgressItems, isValidProgressItem } from './progress-common' import type { ProgressDataPropertiesData } from '../../../item/subtypes/progress' const props = defineProps<{ - excludedSubtypes?: ProgressDataPropertiesData['subtype'][] + excludedSubtypes?: ProgressDataPropertiesData['track']['subtype'][] showCompleted: 'completed-only' | 'no-completed' | 'all' progressStars?: boolean /** From d7b8ded2a56d204ab4a5216aed2d6033bd46bc14 Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:46:54 -0700 Subject: [PATCH 15/46] copy from `progress-model-dirty` --- .../components/progress/progress-track.vue | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/module/vue/components/progress/progress-track.vue b/src/module/vue/components/progress/progress-track.vue index a116b9302..84bcdd4e3 100644 --- a/src/module/vue/components/progress/progress-track.vue +++ b/src/module/vue/components/progress/progress-track.vue @@ -12,8 +12,8 @@ :data-ticks="ticks" :data-score="score" :aria-valuenow="ticks" - :aria-valuemin="ProgressData.TICKS_MIN" - :aria-valuemax="ProgressData.TICKS_MAX" + :aria-valuemin="ProgressTrack.TICKS_MIN" + :aria-valuemax="ProgressTrack.TICKS_MAX" :aria-valuetext="$t('IRONSWORN.PROGRESS.Current', { score, ticks })" :data-tooltip="$t('IRONSWORN.PROGRESS.Current', { score, ticks })"> @@ -31,7 +31,7 @@ import { computed } from 'vue' import { fill } from 'lodash-es' import type { ChallengeRank } from '../../../constants.js' import ProgressTrackBox from './progress-track-box.vue' -import { ProgressData } from '../../../item/subtypes/progress' +import { ProgressTrack } from '../../../model/progress-track' const props = defineProps<{ /** @@ -51,26 +51,26 @@ const props = defineProps<{ const score = computed(() => Math.clamped( - Math.floor(props.ticks / ProgressData.TICKS_PER_BOX), - ProgressData.SCORE_MIN, - ProgressData.SCORE_MAX + Math.floor(props.ticks / ProgressTrack.TICKS_PER_BOX), + ProgressTrack.SCORE_MIN, + ProgressTrack.SCORE_MAX ) ) const visibleTicks = computed(() => - props.ticks > ProgressData.TICKS_MAX - ? props.ticks % ProgressData.TICKS_MAX + props.ticks > ProgressTrack.TICKS_MAX + ? props.ticks % ProgressTrack.TICKS_MAX : props.ticks ) const boxes = computed(() => { - const boxTicks = Array(ProgressData.BOXES) + const boxTicks = Array(ProgressTrack.BOXES) const filledBoxes = Math.floor( - visibleTicks.value / ProgressData.TICKS_PER_BOX + visibleTicks.value / ProgressTrack.TICKS_PER_BOX ) - const ticksRemainder = visibleTicks.value % ProgressData.TICKS_PER_BOX + const ticksRemainder = visibleTicks.value % ProgressTrack.TICKS_PER_BOX - fill(boxTicks, ProgressData.TICKS_PER_BOX, 0, filledBoxes) + fill(boxTicks, ProgressTrack.TICKS_PER_BOX, 0, filledBoxes) if (ticksRemainder > 0) { boxTicks[filledBoxes] = ticksRemainder } From 92db5e6f2f7d2f8b8793f2a42281cd07cf0c3fb1 Mon Sep 17 00:00:00 2001 From: rsek <5354757+rsek@users.noreply.github.com> Date: Fri, 23 Jun 2023 17:06:47 -0700 Subject: [PATCH 16/46] use field statics --- src/module/fields/ChallengeRankField.ts | 35 ++++++++++------ .../vue/components/progress/rank-pips.vue | 41 +++++++++++-------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/module/fields/ChallengeRankField.ts b/src/module/fields/ChallengeRankField.ts index 334220884..983ad168b 100644 --- a/src/module/fields/ChallengeRankField.ts +++ b/src/module/fields/ChallengeRankField.ts @@ -11,6 +11,20 @@ export class ChallengeRankField extends foundry.data.fields.NumberField { Epic: 5 } as const + static readonly i18nKeys = Object.fromEntries( + Object.entries(ChallengeRankField.RANK).map(([key, numericValue]) => [ + numericValue, + `IRONSWORN.CHALLENGERANK.${key}` + ]) + ) as Record + + static readonly RANK_MIN = this.RANK.Troublesome + static readonly RANK_MAX = this.RANK.Epic + + static localizeValue(rank: ChallengeRankField.Rank) { + return game.i18n.localize(ChallengeRankField.i18nKeys[rank]) + } + constructor( options?: Partial< Omit< @@ -21,18 +35,11 @@ export class ChallengeRankField extends foundry.data.fields.NumberField { ) { super({ label: 'IRONSWORN.ChallengeRank', - choices: Object.fromEntries( - Object.entries(ChallengeRankField.RANK).map(([key, numericValue]) => [ - numericValue, - `IRONSWORN.CHALLENGERANK.${key}` - ]) - ) as { - [R in keyof typeof ChallengeRankField.RANK as (typeof ChallengeRankField)['RANK'][R]]: string - }, - initial: ChallengeRankField.RANK.Troublesome as number, + choices: ChallengeRankField.i18nKeys, + initial: ChallengeRankField.RANK_MIN as number, integer: true, - min: ChallengeRankField.RANK.Troublesome, - max: ChallengeRankField.RANK.Epic, + min: ChallengeRankField.RANK_MIN, + max: ChallengeRankField.RANK_MAX, ...options }) } @@ -53,6 +60,10 @@ export class ChallengeRankField extends foundry.data.fields.NumberField { } export interface ChallengeRankField extends foundry.data.fields.NumberField { choices: { - [R in keyof typeof ChallengeRankField.RANK as (typeof ChallengeRankField)['RANK'][R]]: string + [R in ChallengeRankField.Rank]: string } } + +export namespace ChallengeRankField { + export type Rank = ValueOf<(typeof ChallengeRankField)['RANK']> +} diff --git a/src/module/vue/components/progress/rank-pips.vue b/src/module/vue/components/progress/rank-pips.vue index 3a17ed5e0..c094dd11a 100644 --- a/src/module/vue/components/progress/rank-pips.vue +++ b/src/module/vue/components/progress/rank-pips.vue @@ -2,9 +2,9 @@