This commit is contained in:
WBHarry 2026-03-10 22:37:58 +01:00
parent af04fb33d0
commit 62ec3fba38
10 changed files with 263 additions and 64 deletions

View file

@ -1058,6 +1058,10 @@
"fear": "Fear", "fear": "Fear",
"spotlight": "Spotlight" "spotlight": "Spotlight"
}, },
"DaggerheartDiceAnimationEvents": {
"critical": { "name": "Critical" },
"higher": { "name": "Highest Roll" }
},
"DamageType": { "DamageType": {
"physical": { "physical": {
"name": "Physical", "name": "Physical",

View file

@ -12,7 +12,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'daggerheart-appearance-settings', id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'dialog', 'dh-style', 'setting'], classes: ['daggerheart', 'dialog', 'dh-style', 'setting', 'appearance-settings'],
position: { width: '600', height: 'auto' }, position: { width: '600', height: 'auto' },
window: { window: {
title: 'DAGGERHEART.SETTINGS.Menu.title', title: 'DAGGERHEART.SETTINGS.Menu.title',
@ -120,6 +120,9 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
* @protected * @protected
*/ */
async prepareDiceSoNiceContext(context) { async prepareDiceSoNiceContext(context) {
context.animationEvents = CONFIG.DH.GENERAL.daggerheartDiceAnimationEvents;
context.previewAnimation = this.previewAnimation;
context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce( context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce(
(acc, [k, v]) => ({ (acc, [k, v]) => ({
...acc, ...acc,
@ -146,6 +149,13 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
); );
context.diceSoNiceFonts = game.dice3d.exports.Utils.prepareFontList(); context.diceSoNiceFonts = game.dice3d.exports.Utils.prepareFontList();
const getAnimationsOptions = key => {
const fields = context.fields.diceSoNice.fields[key].fields.sfx.fields;
return {
higher: fields.higher.fields.class.choices
};
};
foundry.utils.mergeObject( foundry.utils.mergeObject(
context.dsnTabs, context.dsnTabs,
['hope', 'fear', 'advantage', 'disadvantage'].reduce( ['hope', 'fear', 'advantage', 'disadvantage'].reduce(
@ -153,7 +163,8 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
...acc, ...acc,
[key]: { [key]: {
values: this.setting.diceSoNice[key], values: this.setting.diceSoNice[key],
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields fields: this.setting.schema.getField(`diceSoNice.${key}`).fields,
animations: ['hope', 'fear'].includes(key) ? getAnimationsOptions(key) : {}
} }
}), }),
{} {}
@ -169,8 +180,9 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
* @param {foundry.applications.ux.FormDataExtended} formData * @param {foundry.applications.ux.FormDataExtended} formData
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async #onSubmit(event, form, formData) { static async #onSubmit(_event, _form, formData) {
const data = this.setting.schema.clean(foundry.utils.expandObject(formData.object)); const data = this.setting.schema.clean(foundry.utils.expandObject(formData.object));
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, data); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, data);
} }
@ -183,13 +195,25 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
*/ */
static async #onPreview(_, target) { static async #onPreview(_, target) {
const formData = new foundry.applications.ux.FormDataExtended(target.closest('form')); const formData = new foundry.applications.ux.FormDataExtended(target.closest('form'));
const { diceSoNice } = foundry.utils.expandObject(formData.object); const { diceSoNice, ...rest } = foundry.utils.expandObject(formData.object);
const { key } = target.dataset; const { key } = target.dataset;
const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12'; const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12';
const preset = await getDiceSoNicePreset(diceSoNice[key], faces); const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
const diceSoNiceRoll = await new foundry.dice.Roll(`1${faces}`).evaluate(); const diceSoNiceRoll = await new foundry.dice.Roll(`1${faces}`).evaluate();
diceSoNiceRoll.dice[0].options.appearance = preset.appearance; diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile; diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
const previewAnimation = rest[`${key}PreviewAnimation`];
const events = CONFIG.DH.GENERAL.daggerheartDiceAnimationEvents;
if (previewAnimation) {
if (previewAnimation === events.critical.id && diceSoNice.sfx.critical.class) {
diceSoNiceRoll.dice[0].options.sfx = { specialEffect: diceSoNice.sfx.critical.class };
}
if (previewAnimation === events.higher.id && diceSoNice[key].sfx.higher) {
diceSoNiceRoll.dice[0].options.sfx = { specialEffect: diceSoNice[key].sfx.higher.class };
}
}
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false); await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
} }

View file

@ -630,7 +630,95 @@ export const diceSetNumbers = {
flat: 'Flat' flat: 'Flat'
}; };
export const getDiceSoNicePreset = async (type, faces) => { export const diceSoNiceSFXClasses = {
PlayAnimationBright: {
id: 'PlayAnimationBright',
label: 'DICESONICE.PlayAnimationBright'
},
PlayAnimationDark: {
id: 'PlayAnimationDark',
label: 'DICESONICE.PlayAnimationDark'
},
PlayAnimationOutline: {
id: 'PlayAnimationOutline',
label: 'DICESONICE.PlayAnimationOutline'
},
PlayAnimationImpact: {
id: 'PlayAnimationImpact',
label: 'DICESONICE.PlayAnimationImpact'
},
// PlayConfettiStrength1: {
// id: 'PlayConfettiStrength1',
// label: 'DICESONICE.PlayConfettiStrength1'
// },
// PlayConfettiStrength2: {
// id: 'PlayConfettiStrength2',
// label: 'DICESONICE.PlayConfettiStrength2'
// },
// PlayConfettiStrength3: {
// id: 'PlayConfettiStrength3',
// label: 'DICESONICE.PlayConfettiStrength3'
// },
PlayAnimationThormund: {
id: 'PlayAnimationThormund',
label: 'DICESONICE.PlayAnimationThormund'
},
PlayAnimationParticleSpiral: {
id: 'PlayAnimationParticleSpiral',
label: 'DICESONICE.PlayAnimationParticleSpiral'
},
PlayAnimationParticleSparkles: {
id: 'PlayAnimationParticleSparkles',
label: 'DICESONICE.PlayAnimationParticleSparkles'
},
PlayAnimationParticleVortex: {
id: 'PlayAnimationParticleVortex',
label: 'DICESONICE.PlayAnimationParticleVortex'
},
PlaySoundEpicWin: {
id: 'PlaySoundEpicWin',
label: 'DICESONICE.PlaySoundEpicWin'
},
PlaySoundEpicFail: {
id: 'PlaySoundEpicFail',
label: 'DICESONICE.PlaySoundEpicFail'
}
// "PlaySoundCustom",
// "PlayMacro"
};
export const daggerheartDiceAnimationEvents = {
critical: {
id: 'critical',
label: 'DAGGERHEART.CONFIG.DaggerheartDiceAnimationEvents.critical.name'
},
higher: {
id: 'higher',
label: 'DAGGERHEART.CONFIG.DaggerheartDiceAnimationEvents.higher.name'
}
};
const getDiceSoNiceSFX = sfxOptions => {
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
const criticalAnimationData = diceSoNice.sfx.critical;
if (sfxOptions.critical && criticalAnimationData.class) {
return {
specialEffect: criticalAnimationData.class,
options: {}
};
}
if (sfxOptions.higher && sfxOptions.data.higher) {
return {
specialEffect: sfxOptions.data.higher.class,
options: {}
};
}
return {};
};
export const getDiceSoNicePreset = async (type, faces, sfxOptions = {}) => {
const system = game.dice3d.DiceFactory.systems.get(type.system).dice.get(faces); const system = game.dice3d.DiceFactory.systems.get(type.system).dice.get(faces);
if (!system) { if (!system) {
ui.notifications.error( ui.notifications.error(
@ -653,16 +741,33 @@ export const getDiceSoNicePreset = async (type, faces) => {
appearance: { appearance: {
...system.appearance, ...system.appearance,
...type ...type
} },
sfx: getDiceSoNiceSFX(sfxOptions)
}; };
}; };
export const getDiceSoNicePresets = async (hopeFaces, fearFaces, advantageFaces = 'd6', disadvantageFaces = 'd6') => { export const getDiceSoNicePresets = async (
result,
hopeFaces,
fearFaces,
advantageFaces = 'd6',
disadvantageFaces = 'd6'
) => {
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance); const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
const { isCritical, withHope, withFear } = result;
return { return {
hope: await getDiceSoNicePreset(diceSoNice.hope, hopeFaces), hope: await getDiceSoNicePreset(diceSoNice.hope, hopeFaces, {
fear: await getDiceSoNicePreset(diceSoNice.fear, fearFaces), critical: isCritical,
higher: withHope,
data: diceSoNice.hope.sfx
}),
fear: await getDiceSoNicePreset(diceSoNice.fear, fearFaces, {
critical: isCritical,
higher: withFear,
data: diceSoNice.fear.sfx
}),
advantage: await getDiceSoNicePreset(diceSoNice.advantage, advantageFaces), advantage: await getDiceSoNicePreset(diceSoNice.advantage, advantageFaces),
disadvantage: await getDiceSoNicePreset(diceSoNice.disadvantage, disadvantageFaces) disadvantage: await getDiceSoNicePreset(diceSoNice.disadvantage, disadvantageFaces)
}; };

View file

@ -4,6 +4,16 @@ export default class DhAppearance extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const { StringField, ColorField, BooleanField, SchemaField } = foundry.data.fields; const { StringField, ColorField, BooleanField, SchemaField } = foundry.data.fields;
const sfxSchema = () =>
new SchemaField({
class: new StringField({
nullable: true,
initial: null,
blank: true,
choices: CONFIG.DH.GENERAL.diceSoNiceSFXClasses
})
});
// helper to create dice style schema // helper to create dice style schema
const diceStyle = ({ fg, bg, outline, edge }) => const diceStyle = ({ fg, bg, outline, edge }) =>
new SchemaField({ new SchemaField({
@ -15,7 +25,10 @@ export default class DhAppearance extends foundry.abstract.DataModel {
colorset: new StringField({ initial: 'inspired', required: true, blank: false }), colorset: new StringField({ initial: 'inspired', required: true, blank: false }),
material: new StringField({ initial: 'metal', required: true, blank: false }), material: new StringField({ initial: 'metal', required: true, blank: false }),
system: new StringField({ initial: 'standard', required: true, blank: false }), system: new StringField({ initial: 'standard', required: true, blank: false }),
font: new StringField({ initial: 'auto', required: true, blank: false }) font: new StringField({ initial: 'auto', required: true, blank: false }),
sfx: new SchemaField({
higher: sfxSchema()
})
}); });
return { return {
@ -30,7 +43,10 @@ export default class DhAppearance extends foundry.abstract.DataModel {
hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }), hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }),
fear: diceStyle({ fg: '#000000', bg: '#0032b1', outline: '#ffffff', edge: '#000000' }), fear: diceStyle({ fg: '#000000', bg: '#0032b1', outline: '#ffffff', edge: '#000000' }),
advantage: diceStyle({ fg: '#ffffff', bg: '#008000', outline: '#000000', edge: '#ffffff' }), advantage: diceStyle({ fg: '#ffffff', bg: '#008000', outline: '#000000', edge: '#ffffff' }),
disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' }) disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' }),
sfx: new SchemaField({
critical: sfxSchema()
})
}), }),
extendCharacterDescriptions: new BooleanField(), extendCharacterDescriptions: new BooleanField(),
extendAdversaryDescriptions: new BooleanField(), extendAdversaryDescriptions: new BooleanField(),

View file

@ -378,6 +378,8 @@ export default class DualityRoll extends D20Roll {
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false }); let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false });
const term = parsedRoll.terms[target.dataset.dieIndex]; const term = parsedRoll.terms[target.dataset.dieIndex];
await term.reroll(`/r1=${term.total}`); await term.reroll(`/r1=${term.total}`);
const result = await parsedRoll.evaluate();
if (game.modules.get('dice-so-nice')?.active) { if (game.modules.get('dice-so-nice')?.active) {
const diceSoNiceRoll = { const diceSoNiceRoll = {
_evaluated: true, _evaluated: true,
@ -391,7 +393,7 @@ export default class DualityRoll extends D20Roll {
options: { appearance: {} } options: { appearance: {} }
}; };
const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`); const diceSoNicePresets = await getDiceSoNicePresets(result, `d${term._faces}`, `d${term._faces}`);
const type = target.dataset.type; const type = target.dataset.type;
if (diceSoNicePresets[type]) { if (diceSoNicePresets[type]) {
diceSoNiceRoll.dice[0].options = diceSoNicePresets[type]; diceSoNiceRoll.dice[0].options = diceSoNicePresets[type];
@ -400,8 +402,6 @@ export default class DualityRoll extends D20Roll {
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
} }
await parsedRoll.evaluate();
const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, { const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
targets: message.system.targets, targets: message.system.targets,
roll: { roll: {

View file

@ -60,7 +60,13 @@ export const getCommandTarget = (options = {}) => {
export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, hopeFaces, fearFaces, advantageFaces) => { export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, hopeFaces, fearFaces, advantageFaces) => {
if (!game.modules.get('dice-so-nice')?.active) return; if (!game.modules.get('dice-so-nice')?.active) return;
const diceSoNicePresets = await getDiceSoNicePresets(hopeFaces, fearFaces, advantageFaces, advantageFaces); const diceSoNicePresets = await getDiceSoNicePresets(
rollResult,
hopeFaces,
fearFaces,
advantageFaces,
advantageFaces
);
rollResult.dice[0].options = diceSoNicePresets.hope; rollResult.dice[0].options = diceSoNicePresets.hope;
rollResult.dice[1].options = diceSoNicePresets.fear; rollResult.dice[1].options = diceSoNicePresets.fear;
if (rollResult.dice[2] && advantageState) { if (rollResult.dice[2] && advantageState) {

View file

@ -587,6 +587,13 @@
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 10px; gap: 10px;
> label {
grid-column: span 2;
text-align: center;
font-size: var(--font-size-18);
font-weight: bold;
}
} }
} }
@ -612,11 +619,8 @@
.button-container { .button-container {
display: grid; display: grid;
grid-template-columns: 1fr; align-items: center;
gap: 10px; justify-content: space-between;
text-align: center;
display: flex;
justify-content: center;
width: 100%; width: 100%;
} }
} }

View file

@ -27,6 +27,7 @@
@import './settings/settings.less'; @import './settings/settings.less';
@import './settings/homebrew-settings/domains.less'; @import './settings/homebrew-settings/domains.less';
@import './settings/homebrew-settings/types.less'; @import './settings/homebrew-settings/types.less';
@import './settings/appearance-settings/diceSoNice.less';
@import './sidebar/tabs.less'; @import './sidebar/tabs.less';
@import './sidebar/daggerheartMenu.less'; @import './sidebar/daggerheartMenu.less';

View file

@ -0,0 +1,16 @@
.daggerheart.dh-style.setting.appearance-settings {
.diceSoNice-footer {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 8px;
margin-top: 32px;
label {
text-align: center;
}
select {
grid-column: span 2;
}
}
}

View file

@ -2,6 +2,14 @@
<fieldset> <fieldset>
<div class="title-hint">{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.hint"}}</div> <div class="title-hint">{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.hint"}}</div>
<h3>{{localize "Global Animations"}}</h3>
<div class="split-section">
<div class="label-container full-width">
<label>{{localize "Critical"}}</label>
{{formInput fields.diceSoNice.fields.sfx.fields.critical.fields.class value=setting.diceSoNice.sfx.critical.class blank="" localize=true}}
</div>
</div>
<section class='tab-navigation'> <section class='tab-navigation'>
<div class='navigation-container'> <div class='navigation-container'>
<div class="navigation-inner-container"> <div class="navigation-inner-container">
@ -18,54 +26,69 @@
</div> </div>
</section> </section>
{{#each dsnTabs as |dsnTab|}} {{#each dsnTabs as |dsnTab|}}
<section class="tab {{#if dsnTab.active}} active{{/if}}" data-group="{{dsnTab.group}}" data-tab="{{dsnTab.id}}"> <section class="tab {{#if dsnTab.active}} active{{/if}}" data-group="{{dsnTab.group}}" data-tab="{{dsnTab.id}}">
<div class="field-section"> <div class="field-section">
<div class="label-container full-width"> <h3>{{localize "Dice Appearance"}}</h3>
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.system"}}</label>
{{formInput fields.system value=values.system localize=true choices=@root.diceSoNiceSystems}} <div class="label-container full-width">
</div> <label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.system"}}</label>
<div class="split-section"> {{formInput fields.system value=values.system localize=true choices=@root.diceSoNiceSystems}}
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.foreground"}}</label>
{{formInput fields.foreground value=values.foreground localize=true}}
</div> </div>
<div class="label-container"> <div class="split-section">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.background"}}</label> <div class="label-container">
{{formInput fields.background value=values.background localize=true}} <label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.foreground"}}</label>
{{formInput fields.foreground value=values.foreground localize=true}}
</div>
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.background"}}</label>
{{formInput fields.background value=values.background localize=true}}
</div>
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.outline"}}</label>
{{formInput fields.outline value=values.outline localize=true}}
</div>
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.edge"}}</label>
{{formInput fields.edge value=values.edge localize=true}}
</div>
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.colorset"}}</label>
{{formInput fields.colorset value=values.colorset choices=@root.diceSoNiceColorsets localize=true}}
</div>
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.texture"}}</label>
{{formInput fields.texture value=values.texture choices=@root.diceSoNiceTextures localize=true}}
</div>
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.material"}}</label>
{{formInput fields.material value=values.material choices=@root.diceSoNiceMaterials localize=true}}
</div>
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.font"}}</label>
{{formInput fields.font value=values.font choices=@root.diceSoNiceFonts localize=true}}
</div>
</div> </div>
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.outline"}}</label> {{#if dsnTab.animations}}
{{formInput fields.outline value=values.outline localize=true}} <h3>{{localize "Animations"}}</h3>
</div> <div class="label-container full-width">
<div class="label-container"> <label>{{localize "Highest Roll"}}</label>
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.edge"}}</label> {{formInput fields.sfx.fields.higher.fields.class value=values.sfx.higher.class blank="" localize=true}}
{{formInput fields.edge value=values.edge localize=true}} </div>
</div> {{/if}}
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.colorset"}}</label> <div class="diceSoNice-footer">
{{formInput fields.colorset value=values.colorset choices=@root.diceSoNiceColorsets localize=true}} <label>{{localize "Preview Animation"}}</label>
</div> <select name="{{concat dsnTab.id "PreviewAnimation"}}">
<div class="label-container"> {{selectOptions @root.animationEvents selected=@root.previewAnimation blank="" localize=true }}
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.texture"}}</label> </select>
{{formInput fields.texture value=values.texture choices=@root.diceSoNiceTextures localize=true}} <button type="button" data-action="preview" data-key="{{dsnTab.id}}">
</div> <i class="fa-solid fa-dice"></i>
<div class="label-container"> <span>{{localize "Preview"}}</span>
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.material"}}</label> </button>
{{formInput fields.material value=values.material choices=@root.diceSoNiceMaterials localize=true}}
</div>
<div class="label-container">
<label>{{localize "DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.font"}}</label>
{{formInput fields.font value=values.font choices=@root.diceSoNiceFonts localize=true}}
</div> </div>
</div> </div>
<div class="button-container"> </section>
<button type="button" data-action="preview" data-key="{{dsnTab.id}}">
<i class="fa-solid fa-dice"></i>
<span>{{localize "Preview"}}</span>
</button>
</div>
</div>
</section>
{{/each}} {{/each}}
</fieldset> </fieldset>
</section> </section>