mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-11 19:25:21 +01:00
Merge branch 'main' into #24/Updating-Adversaries
This commit is contained in:
commit
0c50233da3
8 changed files with 37 additions and 194 deletions
|
|
@ -2,4 +2,5 @@ node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
package.json
|
package.json
|
||||||
.github
|
.github
|
||||||
*.hbs
|
*.hbs
|
||||||
|
styles/daggerheart.css
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { MappingField } from './fields.mjs';
|
|
||||||
|
|
||||||
export default class DhpAdversary extends foundry.abstract.TypeDataModel {
|
export default class DhpAdversary extends foundry.abstract.TypeDataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { getTier } from '../helpers/utils.mjs';
|
import { getTier } from '../helpers/utils.mjs';
|
||||||
import DaggerheartAction from './action.mjs';
|
import DaggerheartAction from './action.mjs';
|
||||||
import { MappingField } from './fields.mjs';
|
|
||||||
import DhpEffect from './interface/effects.mjs';
|
import DhpEffect from './interface/effects.mjs';
|
||||||
|
|
||||||
export default class DhpFeature extends DhpEffect {
|
export default class DhpFeature extends DhpEffect {
|
||||||
|
|
@ -26,7 +25,7 @@ export default class DhpFeature extends DhpEffect {
|
||||||
initial: Object.keys(SYSTEM.ACTOR.featureProperties).find(x => x === 'spellcastingTrait')
|
initial: Object.keys(SYSTEM.ACTOR.featureProperties).find(x => x === 'spellcastingTrait')
|
||||||
}),
|
}),
|
||||||
max: new fields.NumberField({ initial: 1, integer: true }),
|
max: new fields.NumberField({ initial: 1, integer: true }),
|
||||||
numbers: new MappingField(
|
numbers: new fields.TypedObjectField(
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
value: new fields.NumberField({ integer: true }),
|
value: new fields.NumberField({ integer: true }),
|
||||||
used: new fields.BooleanField({ initial: false })
|
used: new fields.BooleanField({ initial: false })
|
||||||
|
|
@ -45,7 +44,7 @@ export default class DhpFeature extends DhpEffect {
|
||||||
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
|
multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }),
|
||||||
disabled: new fields.BooleanField({ initial: false }),
|
disabled: new fields.BooleanField({ initial: false }),
|
||||||
description: new fields.HTMLField({}),
|
description: new fields.HTMLField({}),
|
||||||
effects: new MappingField(
|
effects: new fields.TypedObjectField(
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
type: new fields.StringField({ choices: SYSTEM.EFFECTS.effectTypes }),
|
type: new fields.StringField({ choices: SYSTEM.EFFECTS.effectTypes }),
|
||||||
valueType: new fields.StringField({ choices: SYSTEM.EFFECTS.valueTypes }),
|
valueType: new fields.StringField({ choices: SYSTEM.EFFECTS.valueTypes }),
|
||||||
|
|
@ -63,7 +62,7 @@ export default class DhpFeature extends DhpEffect {
|
||||||
{ choices: SYSTEM.EFFECTS.applyLocations },
|
{ choices: SYSTEM.EFFECTS.applyLocations },
|
||||||
{ nullable: true, initial: null }
|
{ nullable: true, initial: null }
|
||||||
),
|
),
|
||||||
applyLocationChoices: new MappingField(new fields.StringField({}), {
|
applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
initial: null
|
initial: null
|
||||||
}),
|
}),
|
||||||
|
|
@ -97,10 +96,4 @@ export default class DhpFeature extends DhpEffect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareDerivedData(){
|
|
||||||
// if(this.featureType.type === SYSTEM.ITEM.valueTypes.dice.id){
|
|
||||||
// this.featureType.numbers = ;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
export class MappingField extends foundry.data.fields.ObjectField {
|
|
||||||
constructor(model, options) {
|
|
||||||
if (!(model instanceof foundry.data.fields.DataField)) {
|
|
||||||
throw new Error('MappingField must have a DataField as its contained element');
|
|
||||||
}
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The embedded DataField definition which is contained in this field.
|
|
||||||
* @type {DataField}
|
|
||||||
*/
|
|
||||||
this.model = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
static get _defaults() {
|
|
||||||
return foundry.utils.mergeObject(super._defaults, {
|
|
||||||
initialKeys: null,
|
|
||||||
initialValue: null,
|
|
||||||
initialKeysOnly: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
_cleanType(value, options) {
|
|
||||||
Object.entries(value).forEach(([k, v]) => (value[k] = this.model.clean(v, options)));
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
getInitialValue(data) {
|
|
||||||
let keys = this.initialKeys;
|
|
||||||
const initial = super.getInitialValue(data);
|
|
||||||
if (!keys || !foundry.utils.isEmpty(initial)) return initial;
|
|
||||||
if (!(keys instanceof Array)) keys = Object.keys(keys);
|
|
||||||
for (const key of keys) initial[key] = this._getInitialValueForKey(key);
|
|
||||||
return initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the initial value for the provided key.
|
|
||||||
* @param {string} key Key within the object being built.
|
|
||||||
* @param {object} [object] Any existing mapping data.
|
|
||||||
* @returns {*} Initial value based on provided field type.
|
|
||||||
*/
|
|
||||||
_getInitialValueForKey(key, object) {
|
|
||||||
const initial = this.model.getInitialValue();
|
|
||||||
return this.initialValue?.(key, initial, object) ?? initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
_validateType(value, options = {}) {
|
|
||||||
if (foundry.utils.getType(value) !== 'Object') throw new Error('must be an Object');
|
|
||||||
const errors = this._validateValues(value, options);
|
|
||||||
if (!foundry.utils.isEmpty(errors)) throw new foundry.data.fields.ModelValidationError(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate each value of the object.
|
|
||||||
* @param {object} value The object to validate.
|
|
||||||
* @param {object} options Validation options.
|
|
||||||
* @returns {Object<Error>} An object of value-specific errors by key.
|
|
||||||
*/
|
|
||||||
_validateValues(value, options) {
|
|
||||||
const errors = {};
|
|
||||||
for (const [k, v] of Object.entries(value)) {
|
|
||||||
const error = this.model.validate(v, options);
|
|
||||||
if (error) errors[k] = error;
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
initialize(value, model, options = {}) {
|
|
||||||
if (!value) return value;
|
|
||||||
const obj = {};
|
|
||||||
const initialKeys = this.initialKeys instanceof Array ? this.initialKeys : Object.keys(this.initialKeys ?? {});
|
|
||||||
const keys = this.initialKeysOnly ? initialKeys : Object.keys(value);
|
|
||||||
for (const key of keys) {
|
|
||||||
const data = value[key] ?? this._getInitialValueForKey(key, value);
|
|
||||||
obj[key] = this.model.initialize(data, model, options);
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
_getField(path) {
|
|
||||||
if (path.length === 0) return this;
|
|
||||||
else if (path.length === 1) return this.model;
|
|
||||||
path.shift();
|
|
||||||
return this.model._getField(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import DaggerheartAction from '../action.mjs';
|
import DaggerheartAction from '../action.mjs';
|
||||||
import { MappingField } from '../fields.mjs';
|
|
||||||
|
|
||||||
export default class DhpEffects extends foundry.abstract.TypeDataModel {
|
export default class DhpEffects extends foundry.abstract.TypeDataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
return {
|
return {
|
||||||
effects: new MappingField(
|
effects: new fields.TypedObjectField(
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
type: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.effectTypes) }),
|
type: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.effectTypes) }),
|
||||||
valueType: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.valueTypes) }),
|
valueType: new fields.StringField({ choices: Object.keys(SYSTEM.EFFECTS.valueTypes) }),
|
||||||
|
|
@ -23,7 +22,7 @@ export default class DhpEffects extends foundry.abstract.TypeDataModel {
|
||||||
{ choices: Object.keys(SYSTEM.EFFECTS.applyLocations) },
|
{ choices: Object.keys(SYSTEM.EFFECTS.applyLocations) },
|
||||||
{ nullable: true, initial: null }
|
{ nullable: true, initial: null }
|
||||||
),
|
),
|
||||||
applyLocationChoices: new MappingField(new fields.StringField({}), {
|
applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
initial: null
|
initial: null
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { getPathValue, getTier } from '../helpers/utils.mjs';
|
import { getPathValue, getTier } from '../helpers/utils.mjs';
|
||||||
import { MappingField } from './fields.mjs';
|
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
|
@ -17,38 +16,21 @@ const attributeField = () =>
|
||||||
});
|
});
|
||||||
|
|
||||||
const levelUpTier = () => ({
|
const levelUpTier = () => ({
|
||||||
attributes: new MappingField(new fields.BooleanField()),
|
attributes: new fields.TypedObjectField(new fields.BooleanField()),
|
||||||
hitPointSlots: new MappingField(new fields.BooleanField()),
|
hitPointSlots: new fields.TypedObjectField(new fields.BooleanField()),
|
||||||
stressSlots: new MappingField(new fields.BooleanField()),
|
stressSlots: new fields.TypedObjectField(new fields.BooleanField()),
|
||||||
experiences: new MappingField(new fields.ArrayField(new fields.StringField({}))),
|
experiences: new fields.TypedObjectField(new fields.ArrayField(new fields.StringField({}))),
|
||||||
proficiency: new MappingField(new fields.BooleanField()),
|
proficiency: new fields.TypedObjectField(new fields.BooleanField()),
|
||||||
armorOrEvasionSlot: new MappingField(new fields.StringField({})),
|
armorOrEvasionSlot: new fields.TypedObjectField(new fields.StringField({})),
|
||||||
majorDamageThreshold2: new MappingField(new fields.BooleanField()),
|
subclass: new fields.TypedObjectField(
|
||||||
severeDamageThreshold2: new MappingField(new fields.BooleanField()),
|
|
||||||
severeDamageThreshold3: new MappingField(new fields.BooleanField()),
|
|
||||||
severeDamageThreshold4: new MappingField(new fields.BooleanField()),
|
|
||||||
subclass: new MappingField(
|
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
multiclass: new fields.BooleanField(),
|
multiclass: new fields.BooleanField(),
|
||||||
feature: new fields.StringField({})
|
feature: new fields.StringField({})
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
multiclass: new MappingField(new fields.BooleanField())
|
multiclass: new fields.TypedObjectField(new fields.BooleanField())
|
||||||
});
|
});
|
||||||
|
|
||||||
// const weapon = () => new fields.SchemaField({
|
|
||||||
// name: new fields.StringField({}),
|
|
||||||
// trait: new fields.StringField({}),
|
|
||||||
// range: new fields.StringField({}),
|
|
||||||
// damage: new fields.SchemaField({
|
|
||||||
// value: new fields.StringField({}),
|
|
||||||
// type: new fields.StringField({}),
|
|
||||||
// }),
|
|
||||||
// feature: new fields.StringField({}),
|
|
||||||
// img: new fields.StringField({}),
|
|
||||||
// uuid: new fields.StringField({}),
|
|
||||||
// }, { initial: null, nullable: true });
|
|
||||||
|
|
||||||
export default class DhpPC extends foundry.abstract.TypeDataModel {
|
export default class DhpPC extends foundry.abstract.TypeDataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -92,16 +74,7 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
|
||||||
min: new fields.NumberField({ initial: 1, integer: true }),
|
min: new fields.NumberField({ initial: 1, integer: true }),
|
||||||
max: new fields.NumberField({ initial: 6, integer: true })
|
max: new fields.NumberField({ initial: 6, integer: true })
|
||||||
}),
|
}),
|
||||||
damageThresholds: new fields.SchemaField({
|
|
||||||
minor: new fields.NumberField({ initial: 0, integer: true }),
|
|
||||||
major: new fields.NumberField({ initial: 0, integer: true }),
|
|
||||||
severe: new fields.NumberField({ initial: 0, integer: true })
|
|
||||||
}),
|
|
||||||
evasion: new fields.NumberField({ initial: 0, integer: true }),
|
evasion: new fields.NumberField({ initial: 0, integer: true }),
|
||||||
// armor: new fields.SchemaField({
|
|
||||||
// value: new fields.NumberField({ initial: 0, integer: true }),
|
|
||||||
// customValue: new fields.NumberField({ initial: null, nullable: true }),
|
|
||||||
// }),
|
|
||||||
experiences: new fields.ArrayField(
|
experiences: new fields.ArrayField(
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
id: new fields.StringField({ required: true }),
|
id: new fields.StringField({ required: true }),
|
||||||
|
|
@ -130,7 +103,7 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
|
||||||
levelData: new fields.SchemaField({
|
levelData: new fields.SchemaField({
|
||||||
currentLevel: new fields.NumberField({ initial: 1, integer: true }),
|
currentLevel: new fields.NumberField({ initial: 1, integer: true }),
|
||||||
changedLevel: new fields.NumberField({ initial: 1, integer: true }),
|
changedLevel: new fields.NumberField({ initial: 1, integer: true }),
|
||||||
levelups: new MappingField(
|
levelups: new fields.TypedObjectField(
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
level: new fields.NumberField({ required: true, integer: true }),
|
level: new fields.NumberField({ required: true, integer: true }),
|
||||||
tier1: new fields.SchemaField({
|
tier1: new fields.SchemaField({
|
||||||
|
|
@ -389,21 +362,35 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
|
||||||
|
|
||||||
this.evasion = this.class?.system?.evasion ?? 0;
|
this.evasion = this.class?.system?.evasion ?? 0;
|
||||||
// this.armor.value = this.activeArmor?.baseScore ?? 0;
|
// this.armor.value = this.activeArmor?.baseScore ?? 0;
|
||||||
this.damageThresholds = this.class?.system?.damageThresholds ?? { minor: 0, major: 0, severe: 0 };
|
this.damageThresholds = this.computeDamageThresholds();
|
||||||
|
|
||||||
this.applyLevels();
|
this.applyLevels();
|
||||||
this.applyEffects();
|
this.applyEffects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
computeDamageThresholds() {
|
||||||
|
// TODO: missing weapon features and domain cards calculation
|
||||||
|
if (!this.armor) {
|
||||||
|
return {
|
||||||
|
major: this.levelData.currentLevel,
|
||||||
|
severe: this.levelData.currentLevel * 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
baseThresholds: { major = 0, severe = 0 }
|
||||||
|
} = this.armor.system;
|
||||||
|
return {
|
||||||
|
major: major + this.levelData.currentLevel,
|
||||||
|
severe: severe + this.levelData.currentLevel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
applyLevels() {
|
applyLevels() {
|
||||||
let healthBonus = 0,
|
let healthBonus = 0,
|
||||||
stressBonus = 0,
|
stressBonus = 0,
|
||||||
proficiencyBonus = 0,
|
proficiencyBonus = 0,
|
||||||
evasionBonus = 0,
|
evasionBonus = 0,
|
||||||
armorBonus = 0,
|
armorBonus = 0;
|
||||||
minorThresholdBonus = 0,
|
|
||||||
majorThresholdBonus = 0,
|
|
||||||
severeThresholdBonus = 0;
|
|
||||||
let experienceBonuses = {};
|
let experienceBonuses = {};
|
||||||
let advancementFirst = null,
|
let advancementFirst = null,
|
||||||
advancementSecond = null;
|
advancementSecond = null;
|
||||||
|
|
@ -439,11 +426,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
|
||||||
armorBonus += Object.keys(tierData.armorOrEvasionSlot).filter(
|
armorBonus += Object.keys(tierData.armorOrEvasionSlot).filter(
|
||||||
x => tierData.armorOrEvasionSlot[x] === 'armor'
|
x => tierData.armorOrEvasionSlot[x] === 'armor'
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
majorThresholdBonus += Object.keys(tierData.majorDamageThreshold2).length * 2;
|
|
||||||
severeThresholdBonus += Object.keys(tierData.severeDamageThreshold2).length * 2;
|
|
||||||
severeThresholdBonus += Object.keys(tierData.severeDamageThreshold3).length * 3;
|
|
||||||
severeThresholdBonus += Object.keys(tierData.severeDamageThreshold4).length * 4;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -456,9 +438,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
|
||||||
max: this.armor ? this.armor.system.marks.max + armorBonus : 0,
|
max: this.armor ? this.armor.system.marks.max + armorBonus : 0,
|
||||||
value: this.armor ? this.armor.system.marks.value : 0
|
value: this.armor ? this.armor.system.marks.value : 0
|
||||||
};
|
};
|
||||||
this.damageThresholds.minor += minorThresholdBonus;
|
|
||||||
this.damageThresholds.major += majorThresholdBonus;
|
|
||||||
this.damageThresholds.severe += severeThresholdBonus;
|
|
||||||
|
|
||||||
this.experiences = this.experiences.map(x => ({ ...x, value: x.value + (experienceBonuses[x.id] ?? 0) }));
|
this.experiences = this.experiences.map(x => ({ ...x, value: x.value + (experienceBonuses[x.id] ?? 0) }));
|
||||||
|
|
||||||
|
|
@ -495,20 +474,6 @@ export default class DhpPC extends foundry.abstract.TypeDataModel {
|
||||||
this.domainData.maxLoadout = Math.min(this.domainData.maxLoadout + 1, 5);
|
this.domainData.maxLoadout = Math.min(this.domainData.maxLoadout + 1, 5);
|
||||||
this.domainData.maxCards += 1;
|
this.domainData.maxCards += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (tier) {
|
|
||||||
case 'tier1':
|
|
||||||
this.damageThresholds.severe += 2;
|
|
||||||
break;
|
|
||||||
case 'tier2':
|
|
||||||
this.damageThresholds.major += 1;
|
|
||||||
this.damageThresholds.severe += 3;
|
|
||||||
break;
|
|
||||||
case 'tier3':
|
|
||||||
this.damageThresholds.major += 2;
|
|
||||||
this.damageThresholds.severe += 4;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "13",
|
"minimum": "13",
|
||||||
"verified": "13.342",
|
"verified": "13.344",
|
||||||
"maximum": "13"
|
"maximum": "13"
|
||||||
},
|
},
|
||||||
"authors": [
|
"authors": [
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
"name": "Darrington Press LLC"
|
"name": "Darrington Press LLC"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "HarryFurAlle"
|
"name": "WBHarry"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "cptn-cosmo"
|
"name": "cptn-cosmo"
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@
|
||||||
<legend class="legend">{{localize "DAGGERHEART.Sheets.PC.Health.Title"}}</legend>
|
<legend class="legend">{{localize "DAGGERHEART.Sheets.PC.Health.Title"}}</legend>
|
||||||
|
|
||||||
<div class="threshold-container">
|
<div class="threshold-container">
|
||||||
<div class="threshold-box">
|
|
||||||
{{document.system.damageThresholds.minor}}
|
|
||||||
</div>
|
|
||||||
<div class="threshold-spacer">
|
<div class="threshold-spacer">
|
||||||
<i class="fa-solid fa-caret-left"></i>
|
|
||||||
<div class="health-category">{{localize "DAGGERHEART.Sheets.PC.Health.Minor"}}</div>
|
<div class="health-category">{{localize "DAGGERHEART.Sheets.PC.Health.Minor"}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue