diff --git a/src/module/powers/powers.js b/src/module/powers/powers.js index 505fb68..3674865 100644 --- a/src/module/powers/powers.js +++ b/src/module/powers/powers.js @@ -56,13 +56,14 @@ import { SmiteEffect } from './smite.js'; import { SoundSilenceEffect } from './soundSilence.js'; import { SpeakLanguageEffect } from './speakLanguage.js'; import { StunEffect } from './stun.js'; -import { SummonAllyEffect, ZombieEffect } from './summon.js'; +import { SummonAllyEffect } from './summon.js'; import { TelekinesisEffect } from './telekinesis.js'; import { TeleportEffect } from './teleport.js'; import { TimeStopEffect } from './timeStop.js'; import { WallWalkerEffect } from './wallWalker.js'; import { WarriorsGiftEffect } from './warriorsGift.js'; import { WishEffect } from './wish.js'; +import { ZombieEffect } from './zombie.js'; const PowerClasses = { 'arcane-protection': ArcaneProtectionEffect, diff --git a/src/module/powers/summon.js b/src/module/powers/summon.js index df20c4d..a665e3c 100644 --- a/src/module/powers/summon.js +++ b/src/module/powers/summon.js @@ -1,50 +1,4 @@ -/* globals Portal */ -import { moduleName } from '../globals.js'; -import { ActorFolderEffect } from './basePowers.js'; - -class BaseSummonEffect extends ActorFolderEffect { - get name() { - return 'Base Summon'; - } - - get actorFolderBase() { - return 'Summonables'; - } - - get summonCount() { - return 0; - } - - async parseValuesPre() {} - - async parseValuesMid() {} - - async parseValuesPost() {} - - async parseValues() { - await super.parseValues(); - await this.parseValuesPre(); - await this.parseValuesMid(); - await this.parseValuesPost(); - } - - async spawn() { - const spawned = await new Portal() - .addCreature(this.targetTokenDoc, { count: this.summonCount }) - .texture(this.targetTokenDoc.texture.src) - .spawn(); - return spawned; - } - - async apply() { - await super.apply(); - const maintainDoc = await this.createMaintainEffect(this.data.maintId); - maintainDoc.flags[moduleName].targetIds = this.data.spawned.map((t) => t.id); - if (this.duration > 0) { - await this.applyActiveEffects(this.source, [maintainDoc]); - } - } -} +import { BaseSummonEffect } from './summonSupport.js'; class BaseAllyEffect extends BaseSummonEffect { get values() { @@ -267,6 +221,10 @@ export class SummonAllyEffect extends BaseAllyEffect { return 'Summon Ally'; } + get actorFolder() { + return `${this.actorFolderBase}/${this.name}`; + } + get icon() { return 'icons/magic/control/silhouette-hold-beam-blue.webp'; } @@ -443,219 +401,3 @@ export class SummonAllyEffect extends BaseAllyEffect { return desc; } } - -export class ZombieEffect extends BaseSummonEffect { - get name() { - return 'Zombie'; - } - - get icon() { - return 'icons/magic/death/hand-dirt-undead-zombie.webp'; - } - - get modifiers() { - const mods = super.modifiers; - mods.push( - { - type: 'number', - name: 'Additional Zombies (+1 per zombie)', - id: 'additionalAllies', - value: 0, - default: 0, - epic: false, - effect: false, - }, - { - type: 'checkbox', - name: 'Armed (+1 per zombie)', - id: 'armed', - value: 0, - default: false, - epic: false, - effect: false, - }, - { - type: 'checkbox', - name: 'Armor (+1 per zombie)', - id: 'armor', - default: false, - epic: false, - effect: true, - icon: 'icons/equipment/chest/breastplate-leather-brown-belted.webp', - changes: [ - { - key: 'system.stats.toughness.armor', - value: 2, - priority: 0, - mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, - }, - ], - }, - { - type: 'checkbox', - name: 'Skeletal (+1 per zombie)', - value: 0, - id: 'skeletal', - epic: false, - default: false, - effect: true, - icon: 'icons/magic/death/hand-undead-skeleton-fire-pink.webp', - changes: [ - { - key: 'system.attributes.agility.die.sides', - value: 2, - priority: 0, - mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, - }, - { - key: '@Skill{Athletics}[system.die.sides]', - value: 2, - priority: 0, - mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, - }, - { - key: '@Skill{Fighting}[system.die.sides]', - value: 2, - priority: 0, - mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, - }, - { - key: '@Skill{Shooting}[system.die.sides]', - value: 2, - priority: 0, - mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, - }, - ], - }, - { - type: 'radio', - name: 'Mind Rider', - id: 'mindRider', - default: 'none', - epic: false, - choices: { none: 'None', single: 'Single', all: 'All' }, - values: { none: 0, single: 1, all: 3 }, - effects: { - none: null, - single: { - name: 'Mind Rider (single)', - icon: 'icons/magic/control/hypnosis-mesmerism-eye.webp', - changes: [], - description: 'The caster can communicate and sense through the ally.', - }, - all: { - name: 'Mind Rider (all)', - icon: 'icons/magic/control/hypnosis-mesmerism-eye.webp', - changes: [], - description: 'The caster can communicate and sense through the ally.', - }, - }, - }, - { - type: 'checkbox', - name: 'Permanent', - id: 'permanent', - default: false, - epic: false, - value: 0, - effect: true, - icon: 'icons/magic/death/skeleton-worn-skull-tan.webp', - changes: [], - description: 'This zombie is permanant until dismissed.', - }, - ); - return mods; - } - - actorValue(actor) { - const size = actor.system.stats.size; - if (size <= -1) { - return 2; - } - return 3 + size; - } - - async parseValuesRaise() { - const attrList = ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor']; - const skillSet = new Set(); - for (const skill of this.targetActor.items.filter((i) => i.type === 'skill')) { - skillSet.add(skill.name); - } - const skillList = Array.from(skillSet); - skillList.sort(); - const html = `

Raise Effect - raise one trait a die type

-

All raised zombies will have the chosen trait raised one die type.

-
- -
-
- `; - const formData = await Dialog.wait({ - title: 'Select trait for raise increase', - content: html, - buttons: { - submit: { - label: 'Submit', - callback: (html) => { - const formElement = html[0].querySelector('form'); - const formData = new FormDataExtended(formElement); - return formData.object; - }, - }, - cancel: { label: 'cancel' }, - }, - }); - const key = formData.trait; - this.data.primaryEffectChanges.push({ - mode: CONST.ACTIVE_EFFECT_MODES.ADD, - value: 2, - key, - priority: 0, - }); - } - - async parseValuesPre() { - await super.parseValuesPre(); - this.data.primaryEffectChanges = []; - this.data.actorUpdates = { name: `${this.source.actor.name}'s raised ${this.targetActor.name}` }; - this.data.tokenUpdates = { name: `${this.source.name}'s raised ${this.targetActor.name}` }; - this.data.embeddedUpdates = { - ActiveEffect: {}, - Item: {}, - }; - if (this.data.armed && this.data.actors['armed_template']) { - const armedTemplate = this.data.actors.armed_template; - for (const item of armedTemplate.items) { - const armedItemDoc = armedTemplate.getEmbeddedDocument('Item', item.id); - this.data.embeddedUpdates.Item[item.name] = armedItemDoc; - } - } - if (this.data.raise) { - await this.parseValuesRaise(); - } - } - - get summonCount() { - return 1 + this.data.additionalAllies; - } - - getPrimaryEffectChanges() { - return [...super.getPrimaryEffectChanges(), ...this.data.primaryEffectChanges]; - } - - get spawnUpdates() { - const updates = super.spawnUpdates; - mergeObject(updates.actor, this.data.actorUpdates); - mergeObject(updates.token, this.data.tokenUpdates); - mergeObject(updates.embedded, this.data.embeddedUpdates); - return updates; - } -} diff --git a/src/module/powers/summonSupport.js b/src/module/powers/summonSupport.js new file mode 100644 index 0000000..bba16e3 --- /dev/null +++ b/src/module/powers/summonSupport.js @@ -0,0 +1,51 @@ +/* globals Portal */ +import { moduleName } from '../globals.js'; +import { ActorFolderEffect } from './basePowers.js'; + +export class BaseSummonEffect extends ActorFolderEffect { + get name() { + return 'Base Summon'; + } + + get actorFolderBase() { + return 'Summonables'; + } + + get actorFolder() { + return `${this.actorFolderBase}/Creatures`; + } + + get summonCount() { + return 0; + } + + async parseValuesPre() {} + + async parseValuesMid() {} + + async parseValuesPost() {} + + async parseValues() { + await super.parseValues(); + await this.parseValuesPre(); + await this.parseValuesMid(); + await this.parseValuesPost(); + } + + async spawn() { + const spawned = await new Portal() + .addCreature(this.targetTokenDoc, { count: this.summonCount }) + .texture(this.targetTokenDoc.texture.src) + .spawn(); + return spawned; + } + + async apply() { + await super.apply(); + const maintainDoc = await this.createMaintainEffect(this.data.maintId); + maintainDoc.flags[moduleName].targetIds = this.data.spawned.map((t) => t.id); + if (this.duration > 0) { + await this.applyActiveEffects(this.source, [maintainDoc]); + } + } +} diff --git a/src/module/powers/zombie.js b/src/module/powers/zombie.js new file mode 100644 index 0000000..1706951 --- /dev/null +++ b/src/module/powers/zombie.js @@ -0,0 +1,262 @@ +import { BaseSummonEffect } from './summonSupport.js'; + +export class ZombieEffect extends BaseSummonEffect { + get name() { + return 'Zombie'; + } + + get icon() { + return 'icons/magic/death/hand-dirt-undead-zombie.webp'; + } + + get duration() { + return 600; + } + + get modifiers() { + const mods = super.modifiers; + mods.push( + { + type: 'number', + name: 'Additional Zombies (+1 per zombie)', + id: 'additionalAllies', + value: 0, + default: 0, + epic: false, + effect: false, + }, + { + type: 'checkbox', + name: 'Armed (+1 per zombie)', + id: 'armed', + value: 0, + default: false, + epic: false, + effect: false, + }, + { + type: 'checkbox', + name: 'Armor (+1 per zombie)', + id: 'armor', + default: false, + value: 0, + epic: false, + effect: true, + icon: 'icons/equipment/chest/breastplate-leather-brown-belted.webp', + changes: [ + { + key: 'system.stats.toughness.armor', + value: 2, + priority: 0, + mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, + }, + ], + }, + { + type: 'checkbox', + name: 'Skeletal (+1 per zombie)', + value: 0, + id: 'skeletal', + epic: false, + default: false, + effect: true, + icon: 'icons/magic/death/hand-undead-skeleton-fire-pink.webp', + changes: [ + { + key: 'system.attributes.agility.die.sides', + value: 2, + priority: 0, + mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, + }, + { + key: '@Skill{Athletics}[system.die.sides]', + value: 2, + priority: 0, + mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, + }, + { + key: '@Skill{Fighting}[system.die.sides]', + value: 2, + priority: 0, + mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, + }, + { + key: '@Skill{Shooting}[system.die.sides]', + value: 2, + priority: 0, + mode: foundry.CONST.ACTIVE_EFFECT_MODES.ADD, + }, + ], + }, + { + type: 'radio', + name: 'Mind Rider', + id: 'mindRider', + default: 'none', + epic: false, + choices: { none: 'None', single: 'Single', all: 'All' }, + values: { none: 0, single: 1, all: 3 }, + effects: { + none: null, + single: { + name: 'Mind Rider (single)', + icon: 'icons/magic/control/hypnosis-mesmerism-eye.webp', + changes: [], + description: 'The caster can communicate and sense through the ally.', + }, + all: { + name: 'Mind Rider (all)', + icon: 'icons/magic/control/hypnosis-mesmerism-eye.webp', + changes: [], + description: 'The caster can communicate and sense through the ally.', + }, + }, + }, + { + type: 'checkbox', + name: 'Permanent', + id: 'permanent', + default: false, + epic: false, + value: 0, + effect: true, + icon: 'icons/magic/death/skeleton-worn-skull-tan.webp', + changes: [], + description: 'This zombie is permanant until dismissed.', + }, + ); + return mods; + } + + actorValue(actor) { + const size = actor.system.stats.size; + if (size <= -1) { + return 2; + } + return 3 + size; + } + + async parseValuesRaise() { + const attrList = ['Agility', 'Smarts', 'Spirit', 'Strength', 'Vigor']; + const skillSet = new Set(); + for (const skill of this.targetActor.items.filter((i) => i.type === 'skill')) { + skillSet.add(skill.name); + } + const skillList = Array.from(skillSet); + skillList.sort(); + const html = `

Raise Effect - raise one trait a die type

+

All raised zombies will have the chosen trait raised one die type.

+
+ +
+
+ `; + const formData = await Dialog.wait({ + title: 'Select trait for raise increase', + content: html, + buttons: { + submit: { + label: 'Submit', + callback: (html) => { + const formElement = html[0].querySelector('form'); + const formData = new FormDataExtended(formElement); + return formData.object; + }, + }, + cancel: { label: 'cancel' }, + }, + }); + const key = formData.trait; + this.data.primaryEffectChanges.push({ + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: 2, + key, + priority: 0, + }); + } + + async parseValuesPre() { + await super.parseValuesPre(); + this.data.primaryEffectChanges = []; + this.data.actorUpdates = { name: `${this.source.actor.name}'s raised ${this.targetActor.name}` }; + this.data.tokenUpdates = { name: `${this.source.name}'s raised ${this.targetActor.name}` }; + this.data.embeddedUpdates = { + ActiveEffect: {}, + Item: {}, + }; + if (this.data.armed && this.data.actors['armed_template']) { + const armedTemplate = this.data.actors.armed_template; + for (const item of armedTemplate.items) { + const armedItemDoc = armedTemplate.getEmbeddedDocument('Item', item.id); + this.data.embeddedUpdates.Item[item.name] = armedItemDoc; + } + } + if (this.data.raise) { + await this.parseValuesRaise(); + } + } + + get summonCount() { + return 1 + this.data.additionalAllies; + } + + getPrimaryEffectChanges() { + return [...super.getPrimaryEffectChanges(), ...this.data.primaryEffectChanges]; + } + + get powerPoints() { + const basicZombieNames = ['zombie', 'human zombie', 'zombie, human']; + let total = super.powerPoints; + const base = this.actorValue(this.targetActor); + let additional = basicZombieNames.includes(this.targetActor.name.toLowerCase()) + ? 1 + : Math.max(1, Math.ceil(base / 2)); + total += (this.summonCount - 1) * additional; + const mods = [this.data.armed, this.data.armor, this.data.skeletal]; + total += mods.map((v) => (v ? 1 : 0)).reduce((a, b) => a + b) * this.summonCount; + return total; + } + + get spawnUpdates() { + const updates = super.spawnUpdates; + mergeObject(updates.actor, this.data.actorUpdates); + mergeObject(updates.token, this.data.tokenUpdates); + mergeObject(updates.embedded, this.data.embeddedUpdates); + return updates; + } + + get description() { + let desc = super.description; + const summonCount = this.summonCount; + const plural = summonCount > 1 ? 's' : ''; + desc += `

Summon ${summonCount} ${this.targetActor.name}${plural}. + `; + if (this.data.armed || this.data.armor) { + desc += `The zombie${plural} are `; + if (this.data.armed) { + desc += 'armed '; + } + if (this.data.armed && this.data.armor) { + desc += 'and '; + } + if (this.data.armor) { + desc += 'armored '; + } + desc += 'with rotting but working equipment. '; + } + if (this.data.skeletal) { + desc += `These particlar zombies have shed the bulk of their flesh, becoming + stronger but more skeletal in the process.`; + } + desc += '

'; + return desc; + } +}