const fields$e = foundry.data.fields; function createCharacteristicField(show = true, showMax = false) { return new fields$e.SchemaField({ value: new fields$e.NumberField({ required: true, initial: 0, min: 0, integer: true }), max: new fields$e.NumberField({ required: false, initial: 0, min: 0, integer: true }), dm: new fields$e.NumberField({ required: false, initial: 0, integer: true }), show: new fields$e.BooleanField({ required: false, initial: show }), showMax: new fields$e.BooleanField({ required: false, initial: showMax }) }); } class ItemBaseData extends foundry.abstract.TypeDataModel { static defineSchema() { return { description: new fields$e.HTMLField({ required: false, blank: true, trim: true }), subType: new fields$e.StringField({ required: false, blank: false, nullable: true }) }; } } class PhysicalItemData extends ItemBaseData { static defineSchema() { const schema = super.defineSchema(); schema.quantity = new fields$e.NumberField({ required: true, initial: 1, min: 0, integer: true }); schema.weight = new fields$e.NumberField({ required: true, initial: 0, min: 0, integer: false }); schema.weightless = new fields$e.BooleanField({ required: false, initial: false }); schema.cost = new fields$e.NumberField({ required: true, initial: 0, min: 0, integer: true }); schema.tl = new fields$e.StringField({ required: true, blank: false, initial: "TL12" }); schema.container = new fields$e.SchemaField({ id: new fields$e.StringField({ required: false, blank: true }) }); schema.roll = new fields$e.SchemaField({ characteristic: new fields$e.StringField({ required: false, blank: true, trim: true }), skill: new fields$e.StringField({ required: false, blank: true, trim: true }), difficulty: new fields$e.StringField({ required: false, blank: true, trim: true }) }); schema.trash = new fields$e.BooleanField({ required: false, initial: false }); return schema; } } const fields$d = foundry.data.fields; class CharacterData extends foundry.abstract.TypeDataModel { static defineSchema() { return { name: new fields$d.StringField({ required: false, blank: false, trim: true }), life: new fields$d.SchemaField({ value: new fields$d.NumberField({ required: false, initial: 0, integer: true }), max: new fields$d.NumberField({ required: true, initial: 0, integer: true }) }), personal: new fields$d.SchemaField({ title: new fields$d.StringField({ required: false, blank: true, trim: true }), species: new fields$d.StringField({ required: false, blank: true, trim: true }), speciesText: new fields$d.SchemaField({ description: new fields$d.StringField({ required: false, blank: true, trim: true, nullable: true }), descriptionLong: new fields$d.HTMLField({ required: false, blank: true, trim: true }) }), age: new fields$d.StringField({ required: false, blank: true, trim: true }), gender: new fields$d.StringField({ required: false, blank: true, trim: true }), pronouns: new fields$d.StringField({ required: false, blank: true, trim: true }), homeworld: new fields$d.StringField({ required: false, blank: true, trim: true }), ucp: new fields$d.StringField({ required: false, blank: true, trim: true, initial: "" }), traits: new fields$d.ArrayField( new fields$d.SchemaField({ name: new fields$d.StringField({ required: true, blank: true, trim: true }), description: new fields$d.StringField({ required: false, blank: true, trim: true }) }) ) }), biography: new fields$d.HTMLField({ required: false, blank: true, trim: true }), characteristics: new fields$d.SchemaField({ strength: createCharacteristicField(true, true), dexterity: createCharacteristicField(true, true), endurance: createCharacteristicField(true, true), intellect: createCharacteristicField(true, false), education: createCharacteristicField(true, false), social: createCharacteristicField(true, false), morale: createCharacteristicField(true, false), luck: createCharacteristicField(true, false), sanity: createCharacteristicField(true, false), charm: createCharacteristicField(true, false), psionic: createCharacteristicField(true, false), other: createCharacteristicField(true, false) }), health: new fields$d.SchemaField({ radiations: new fields$d.NumberField({ required: false, initial: 0, min: 0, integer: true }), lastFirstAidDate: new fields$d.StringField({ required: false, blank: true, trim: true }), healingRecoveryMode: new fields$d.StringField({ required: false, blank: true, trim: true, initial: "" }) }), study: new fields$d.SchemaField({ skill: new fields$d.StringField({ required: false, blank: true, trim: true, initial: "" }), total: new fields$d.NumberField({ required: false, initial: 0, min: 0, integer: true }), completed: new fields$d.NumberField({ required: false, initial: 0, min: 0, integer: true }) }), finance: new fields$d.SchemaField({ pension: new fields$d.NumberField({ required: true, initial: 0, min: 0, integer: true }), credits: new fields$d.NumberField({ required: true, initial: 0, min: 0, integer: true }), cashOnHand: new fields$d.NumberField({ required: true, initial: 0, min: 0, integer: true }), debt: new fields$d.NumberField({ required: true, initial: 0, min: 0, integer: true }), livingCost: new fields$d.NumberField({ required: true, initial: 0, min: 0, integer: true }), monthlyShipPayments: new fields$d.NumberField({ required: true, initial: 0, min: 0, integer: true }), notes: new fields$d.HTMLField({ required: false, blank: true, trim: true }) }), containerView: new fields$d.StringField({ required: false, blank: true, trim: true, initial: "" }), containerDropIn: new fields$d.StringField({ required: false, blank: true, trim: true, initial: "" }), notes: new fields$d.HTMLField({ required: false, blank: true, trim: true }), inventory: new fields$d.SchemaField({ armor: new fields$d.NumberField({ required: true, initial: 0, integer: true }), weight: new fields$d.NumberField({ required: true, initial: 0, min: 0, integer: false }), encumbrance: new fields$d.SchemaField({ normal: new fields$d.NumberField({ required: true, initial: 0, min: 0, integer: true }), heavy: new fields$d.NumberField({ required: true, initial: 0, min: 0, integer: true }) }) }), states: new fields$d.SchemaField({ encumbrance: new fields$d.BooleanField({ required: false, initial: false }), fatigue: new fields$d.BooleanField({ required: false, initial: false }), unconscious: new fields$d.BooleanField({ required: false, initial: false }), surgeryRequired: new fields$d.BooleanField({ required: false, initial: false }) }), config: new fields$d.SchemaField({ psionic: new fields$d.BooleanField({ required: false, initial: true }), initiative: new fields$d.StringField({ required: false, blank: true, initial: "dexterity" }), damages: new fields$d.SchemaField({ rank1: new fields$d.StringField({ required: false, blank: true, initial: "strength" }), rank2: new fields$d.StringField({ required: false, blank: true, initial: "dexterity" }), rank3: new fields$d.StringField({ required: false, blank: true, initial: "endurance" }) }) }) }; } } const fields$c = foundry.data.fields; class VehiculeData extends foundry.abstract.TypeDataModel { static defineSchema() { return { name: new fields$c.StringField({ required: false, blank: false, trim: true }), skillId: new fields$c.StringField({ required: false, initial: "", blank: true, trim: true }), speed: new fields$c.SchemaField({ cruise: new fields$c.StringField({ required: false, initial: "Slow", blank: true }), maximum: new fields$c.StringField({ required: false, initial: "Medium", blank: true }) }), agility: new fields$c.NumberField({ required: false, min: 0, integer: true }), crew: new fields$c.NumberField({ required: false, min: 0, integer: true }), passengers: new fields$c.NumberField({ required: false, min: 0, integer: true }), cargo: new fields$c.NumberField({ required: false, min: 0, integer: false }), life: new fields$c.SchemaField({ value: new fields$c.NumberField({ required: true, initial: 0, integer: true }), max: new fields$c.NumberField({ required: true, initial: 0, integer: true }) }), shipping: new fields$c.NumberField({ required: false, min: 0, integer: true }), cost: new fields$c.NumberField({ required: false, min: 0, integer: true }), armor: new fields$c.SchemaField({ front: new fields$c.NumberField({ required: true, initial: 0, integer: true }), rear: new fields$c.NumberField({ required: true, initial: 0, integer: true }), sides: new fields$c.NumberField({ required: true, initial: 0, integer: true }) }), skills: new fields$c.SchemaField({ autopilot: new fields$c.NumberField({ required: true, initial: 0, integer: true }) }), description: new fields$c.HTMLField({ required: false, blank: true, trim: true }), notes: new fields$c.HTMLField({ required: false, blank: true, trim: true }) }; } } const fields$b = foundry.data.fields; class CreatureData extends foundry.abstract.TypeDataModel { static defineSchema() { return { life: new fields$b.SchemaField({ value: new fields$b.NumberField({ required: true, initial: 10, min: 0, integer: true }), max: new fields$b.NumberField({ required: true, initial: 10, min: 0, integer: true }) }), speed: new fields$b.NumberField({ required: true, initial: 6, min: 0, integer: true }), armor: new fields$b.NumberField({ required: true, initial: 0, min: 0, integer: true }), psi: new fields$b.NumberField({ required: true, initial: 0, min: 0, integer: true }), initiativeBonus: new fields$b.NumberField({ required: true, initial: 0, integer: true }), skills: new fields$b.ArrayField( new fields$b.SchemaField({ name: new fields$b.StringField({ required: true, blank: true, trim: true, initial: "" }), level: new fields$b.NumberField({ required: true, initial: 0, integer: true }), note: new fields$b.StringField({ required: false, blank: true, trim: true, initial: "" }) }) ), attacks: new fields$b.ArrayField( new fields$b.SchemaField({ name: new fields$b.StringField({ required: true, blank: true, trim: true, initial: "" }), damage: new fields$b.StringField({ required: true, blank: true, trim: true, initial: "1D" }), skill: new fields$b.NumberField({ required: false, initial: -1, integer: true }), description: new fields$b.StringField({ required: false, blank: true, trim: true, initial: "" }) }) ), traits: new fields$b.ArrayField( new fields$b.SchemaField({ name: new fields$b.StringField({ required: true, blank: true, trim: true, initial: "" }), value: new fields$b.StringField({ required: false, blank: true, trim: true, initial: "" }), description: new fields$b.StringField({ required: false, blank: true, trim: true, initial: "" }) }) ), behavior: new fields$b.SchemaField({ type: new fields$b.StringField({ required: false, blank: true, trim: true, initial: "" }), subtype: new fields$b.StringField({ required: false, blank: true, trim: true, initial: "" }) }), biography: new fields$b.HTMLField({ required: false, blank: true, trim: true }), notes: new fields$b.HTMLField({ required: false, blank: true, trim: true }), }; } /** @override */ prepareDerivedData() { // Compute initiative bonus from Métabolisme traits let bonus = 0; for (const trait of this.traits) { const nameLower = trait.name.toLowerCase(); if (nameLower.includes("métabolisme rapide") || nameLower.includes("metabolisme rapide")) { const val = parseInt(trait.value); if (!isNaN(val)) bonus += val; } else if (nameLower.includes("métabolisme lent") || nameLower.includes("metabolisme lent")) { const val = parseInt(trait.value); if (!isNaN(val)) bonus -= val; } } this.initiativeBonus = bonus; // Compute armor from Armure trait if not set manually if (this.armor === 0) { for (const trait of this.traits) { if (trait.name.toLowerCase().startsWith("armure")) { const val = parseInt(trait.value); if (!isNaN(val)) { this.armor = val; break; } } } } // Compute PSI from Psionique trait if (this.psi === 0) { for (const trait of this.traits) { if (trait.name.toLowerCase().startsWith("psionique")) { const val = parseInt(trait.value); if (!isNaN(val)) { this.psi = val; break; } } } } } } const fields$a = foundry.data.fields; class ItemData extends PhysicalItemData { static defineSchema() { const schema = super.defineSchema(); schema.subType.initial = "loot"; schema.software = new fields$a.SchemaField({ bandwidth: new fields$a.NumberField({ required: false, initial: 0, min: 0, max: 10, integer: true }), effect: new fields$a.StringField({ required: false, blank: true, trim: true, initial: "" }), computerId: new fields$a.StringField({ required: false, blank: true, initial: "" }) }); return schema; } } const fields$9 = foundry.data.fields; class EquipmentData extends PhysicalItemData { static defineSchema() { const schema = super.defineSchema(); schema.equipped = new fields$9.BooleanField({ required: false, initial: false }); schema.augment = new fields$9.SchemaField({ improvement: new fields$9.StringField({ required: false, blank: true, trim: true }) }); schema.subType.initial = "equipment"; // augment, clothing, trinket, toolkit, equipment return schema; } } const fields$8 = foundry.data.fields; class DiseaseData extends ItemBaseData { static defineSchema() { const schema = super.defineSchema(); schema.subType.initial = "disease"; // disease, poison schema.difficulty = new fields$8.StringField({ required: true, initial: "Average" }); schema.damage = new fields$8.StringField({ required: false, blank: true }); schema.interval = new fields$8.StringField({ required: false, blank: true }); return schema; } } const fields$7 = foundry.data.fields; class CareerData extends ItemBaseData { static defineSchema() { const schema = super.defineSchema(); schema.difficulty = new fields$7.NumberField({ required: true, initial: 0, min: 0, integer: true }); schema.damage = new fields$7.StringField({ required: false, blank: true }); schema.interval = new fields$7.StringField({ required: false, blank: true }); schema.assignment = new fields$7.StringField({ required: false, blank: true }); schema.terms = new fields$7.NumberField({ required: false, initial: 0, min: 0, integer: true }); schema.rank = new fields$7.NumberField({ required: false, initial: 0, min: 0, integer: true }); schema.events = new fields$7.ArrayField( new fields$7.SchemaField({ age: new fields$7.NumberField({ required: false, integer: true }), description: new fields$7.StringField({ required: false, blank: true, trim: true }) }) ); return schema; } } const fields$6 = foundry.data.fields; class TalentData extends ItemBaseData { static defineSchema() { const schema = super.defineSchema(); schema.subType.initial = "skill"; schema.cost = new fields$6.NumberField({ required: true, initial: 0, min: 0, integer: true }); schema.level = new fields$6.NumberField({ required: true, initial: 0, min: 0, integer: true }); schema.skill = new fields$6.SchemaField({ speciality: new fields$6.StringField({ required: false, blank: true, trim: true }), reduceEncumbrance: new fields$6.BooleanField({ required: false, initial: false }) }); schema.psionic = new fields$6.SchemaField({ reach: new fields$6.StringField({ required: false, blank: true, trim: true }), cost: new fields$6.NumberField({ required: false, initial: 1, min: 0, integer: true }), duration: new fields$6.StringField({ required: false, blank: true, trim: true }), durationUnit: new fields$6.StringField({ required: false }) }); schema.roll = new fields$6.SchemaField({ characteristic: new fields$6.StringField({ required: false, blank: true, trim: true }), skill: new fields$6.StringField({ required: false, blank: true, trim: true }), difficulty: new fields$6.StringField({ required: false, blank: true, trim: true }) }); return schema; } } const fields$5 = foundry.data.fields; class ContactData extends ItemBaseData { static defineSchema() { const schema = super.defineSchema(); schema.subType.initial = "skill"; schema.cost = new fields$5.NumberField({ required: true, initial: 1, min: 0, integer: true }); schema.skill = new fields$5.SchemaField({ speciality: new fields$5.StringField({ required: false, blank: true, trim: true }), characteristic: new fields$5.StringField({ required: false, blank: true, trim: true }) }); schema.status = new fields$5.StringField({ required: false, blank: true, trim: true, initial: "Alive" }); schema.attitude = new fields$5.StringField({ required: false, blank: true, trim: true, initial: "Unknow" }); schema.relation = new fields$5.StringField({ required: false, blank: true, trim: true, initial: "Contact" }); schema.title = new fields$5.StringField({ required: false, blank: true, trim: true }); schema.nickname = new fields$5.StringField({ required: false, blank: true, trim: true }); schema.species = new fields$5.StringField({ required: false, blank: true, trim: true }); schema.gender = new fields$5.StringField({ required: false, blank: true, trim: true }); schema.pronouns = new fields$5.StringField({ required: false, blank: true, trim: true }); schema.homeworld = new fields$5.StringField({ required: false, blank: true, trim: true }); schema.location = new fields$5.StringField({ required: false, blank: true, trim: true }); schema.occupation = new fields$5.StringField({ required: false, blank: true, trim: true }); schema.notes = new fields$5.HTMLField({ required: false, blank: true, trim: true }); return schema; } } const fields$4 = foundry.data.fields; class WeaponData extends PhysicalItemData { static defineSchema() { const schema = super.defineSchema(); schema.equipped = new fields$4.BooleanField({ required: false, initial: false }); schema.range = new fields$4.SchemaField({ isMelee: new fields$4.BooleanField({ required: false, initial: false }), value: new fields$4.NumberField({ required: false, integer: true, nullable: true }), unit: new fields$4.StringField({ required: false, blank: true, nullable: true }) }); schema.damage = new fields$4.StringField({ required: false, blank: true, trim: true }); schema.magazine = new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }); schema.magazineCost = new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }); schema.traits = new fields$4.ArrayField( new fields$4.SchemaField({ name: new fields$4.StringField({ required: true, blank: true, trim: true }), description: new fields$4.StringField({ required: false, blank: true, trim: true }) }) ); schema.options = new fields$4.ArrayField( new fields$4.SchemaField({ name: new fields$4.StringField({ required: true, blank: true, trim: true }), description: new fields$4.StringField({ required: false, blank: true, trim: true }) }) ); return schema; } } const fields$3 = foundry.data.fields; class ArmorData extends PhysicalItemData { static defineSchema() { const schema = super.defineSchema(); schema.equipped = new fields$3.BooleanField({ required: false, initial: false }); schema.radiations = new fields$3.NumberField({ required: false, initial: 0, min: 0, integer: true }); schema.protection = new fields$3.StringField({ required: false, blank: false, trim: true }); // A Traveller suffers DM-1 to all checks per missing skill level in the required skill. schema.requireSkill = new fields$3.StringField({ required: false, blank: false }); schema.requireSkillLevel = new fields$3.NumberField({ required: false, min: 0, integer: true }); // Powered armour supports its own weight and is effectively weightless for encumbrance. schema.powered = new fields$3.BooleanField({ required: false, initial: false }); schema.options = new fields$3.ArrayField( new fields$3.SchemaField({ name: new fields$3.StringField({ required: true, blank: true, trim: true }), description: new fields$3.StringField({ required: false, blank: true, trim: true }) }) ); return schema; } } const fields$2 = foundry.data.fields; class ComputerData extends PhysicalItemData { static defineSchema() { const schema = super.defineSchema(); schema.processing = new fields$2.NumberField({ required: false, initial: 0, min: 0, integer: true }); schema.processingUsed = new fields$2.NumberField({ required: false, initial: 0, min: 0, integer: true }); schema.overload = new fields$2.BooleanField({ required: false, initial: false }); schema.options = new fields$2.ArrayField( new fields$2.SchemaField({ name: new fields$2.StringField({ required: true, blank: true, trim: true }), description: new fields$2.StringField({ required: false, blank: true, trim: true }) }) ); return schema; } } const fields$1 = foundry.data.fields; class ItemContainerData extends ItemBaseData { static defineSchema() { const schema = super.defineSchema(); schema.onHand = new fields$1.BooleanField({ required: false, initial: false }); schema.location = new fields$1.StringField({ required: false, blank: true, trim: true }); schema.count = new fields$1.NumberField({ required: false, initial: 0, integer: true }); schema.weight = new fields$1.NumberField({ required: false, initial: 0, integer: false }); schema.weightless = new fields$1.BooleanField({ required: false, initial: false }); schema.locked = new fields$1.BooleanField({ required: false, initial: false }); // GM only schema.lockedDescription = new fields$1.HTMLField({ required: false, blank: true, trim: true }); return schema; } } const fields = foundry.data.fields; class SpeciesData extends foundry.abstract.TypeDataModel { static defineSchema() { return { description: new fields.HTMLField({ required: false, blank: true, trim: true }), descriptionLong: new fields.HTMLField({ required: false, blank: true, trim: true }), traits: new fields.ArrayField( new fields.SchemaField({ name: new fields.StringField({ required: true, blank: true, trim: true }), description: new fields.StringField({ required: false, blank: true, trim: true }) }) ), modifiers: new fields.ArrayField( new fields.SchemaField({ characteristic: new fields.StringField({ required: false, blank: true, trim: true }), value: new fields.NumberField({ required: false, integer: true, nullable: true }) }) ) }; } } const MGT2 = {}; MGT2.MetricRange = Object.freeze({ meter: "MGT2.MetricRange.meter", kilometer: "MGT2.MetricRange.kilometer" }); MGT2.MetricWeight = Object.freeze({ kilogram: "MGT2.MetricWeight.kilogram", ton: "MGT2.MetricWeight.ton" }); MGT2.Difficulty = Object.freeze({ NA: "MGT2.Difficulty.NA", Simple: "MGT2.Difficulty.Simple", Easy: "MGT2.Difficulty.Easy", Routine: "MGT2.Difficulty.Routine", Average: "MGT2.Difficulty.Average", Difficult: "MGT2.Difficulty.Difficult", VeryDifficult: "MGT2.Difficulty.VeryDifficult", Formidable: "MGT2.Difficulty.Formidable", Impossible: "MGT2.Difficulty.Impossible" }); MGT2.ItemSubType = Object.freeze({ loot: "MGT2.ItemSubType.loot", software: "MGT2.ItemSubType.software" }); MGT2.EquipmentSubType = Object.freeze({ augment: "MGT2.EquipmentSubType.augment", clothing: "MGT2.EquipmentSubType.clothing", equipment: "MGT2.EquipmentSubType.equipment", trinket: "MGT2.EquipmentSubType.trinket", toolkit: "MGT2.EquipmentSubType.toolkit" }); MGT2.TalentSubType = Object.freeze({ skill: "MGT2.TalentSubType.skill", psionic: "MGT2.TalentSubType.psionic" }); MGT2.DiseaseSubType = Object.freeze({ disease: "MGT2.DiseaseSubType.disease", poison: "MGT2.DiseaseSubType.poison", wound: "MGT2.DiseaseSubType.wound" }); MGT2.PsionicReach = Object.freeze({ NA: "MGT2.PsionicReach.NA", Personal: "MGT2.PsionicReach.Personal", Close: "MGT2.PsionicReach.Close", Short: "MGT2.PsionicReach.Short", Medium: "MGT2.PsionicReach.Medium", Long: "MGT2.PsionicReach.Long", VeryLong: "MGT2.PsionicReach.VeryLong", Distant: "MGT2.PsionicReach.Distant", VeryDistant: "MGT2.PsionicReach.VeryDistant", Continental: "MGT2.PsionicReach.Continental", Planetary: "MGT2.PsionicReach.Planetary" }); MGT2.ContactRelations = Object.freeze({ Allie: "MGT2.Contact.Relation.Allie", Contact: "MGT2.Contact.Relation.Contact", Rival: "MGT2.Contact.Relation.Rival", Enemy: "MGT2.Contact.Relation.Enemy" }); MGT2.ContactStatus = Object.freeze({ Alive: "MGT2.Contact.Status.Alive", Unknow: "MGT2.Contact.Status.Unknow", Dead: "MGT2.Contact.Status.Dead" }); MGT2.Attitudes = Object.freeze({ Unknow: "MGT2.Contact.Attitude.Unknow", Hostile: "MGT2.Contact.Attitude.Hostile", Unfriendly: "MGT2.Contact.Attitude.Unfriendly", Indifferent: "MGT2.Contact.Attitude.Indifferent", Friendly: "MGT2.Contact.Attitude.Friendly", Helpful: "MGT2.Contact.Attitude.Helpful", Complicated: "MGT2.Contact.Attitude.Complicated" }); MGT2.Characteristics = Object.freeze({ strength: "MGT2.Characteristics.strength.name", dexterity: "MGT2.Characteristics.dexterity.name", endurance: "MGT2.Characteristics.endurance.name", intellect: "MGT2.Characteristics.intellect.name", education: "MGT2.Characteristics.education.name", social: "MGT2.Characteristics.social.name", morale: "MGT2.Characteristics.morale.name", luck: "MGT2.Characteristics.luck.name", sanity: "MGT2.Characteristics.sanity.name", charm: "MGT2.Characteristics.charm.name", psionic: "MGT2.Characteristics.psionic.name", other: "MGT2.Characteristics.other.name" }); MGT2.InitiativeCharacteristics = Object.freeze({ dexterity: "MGT2.Characteristics.dexterity.name", intellect: "MGT2.Characteristics.intellect.name" }); MGT2.DamageCharacteristics = Object.freeze({ strength: "MGT2.Characteristics.strength.name", dexterity: "MGT2.Characteristics.dexterity.name", endurance: "MGT2.Characteristics.endurance.name" }); MGT2.TL = Object.freeze({ NA: "MGT2.TL.NA", Unknow: "MGT2.TL.Unknow", NotIdentified: "MGT2.TL.NotIdentified", TL00: "MGT2.TL.L00", TL01: "MGT2.TL.L01", TL02: "MGT2.TL.L02", TL03: "MGT2.TL.L03", TL04: "MGT2.TL.L04", TL05: "MGT2.TL.L05", TL06: "MGT2.TL.L06", TL07: "MGT2.TL.L07", TL08: "MGT2.TL.L08", TL09: "MGT2.TL.L09", TL10: "MGT2.TL.L10", TL11: "MGT2.TL.L11", TL12: "MGT2.TL.L12", TL13: "MGT2.TL.L13", TL14: "MGT2.TL.L14", TL15: "MGT2.TL.L15" }); MGT2.Timeframes = Object.freeze({ Normal: "MGT2.Timeframes.Normal", Slower: "MGT2.Timeframes.Slower", Faster: "MGT2.Timeframes.Faster" }); MGT2.SpeedBands = Object.freeze({ Stoppped: "MGT2.SpeedBands.Stoppped", Idle: "MGT2.SpeedBands.Idle", VerySlow: "MGT2.SpeedBands.VerySlow", Slow: "MGT2.SpeedBands.Slow", Medium: "MGT2.SpeedBands.Medium", High: "MGT2.SpeedBands.High", Fast: "MGT2.SpeedBands.Fast", VeryFast: "MGT2.SpeedBands.VeryFast", Subsonic: "MGT2.SpeedBands.Subsonic", Hypersonic: "MGT2.SpeedBands.Hypersonic" }); MGT2.Durations = Object.freeze({ Seconds: "MGT2.Durations.Seconds", Minutes: "MGT2.Durations.Minutes", Heures: "MGT2.Durations.Heures" }); MGT2.CreatureBehaviorType = Object.freeze({ herbivore: "MGT2.CreatureBehaviorType.herbivore", carnivore: "MGT2.CreatureBehaviorType.carnivore", charognard: "MGT2.CreatureBehaviorType.charognard", omnivore: "MGT2.CreatureBehaviorType.omnivore" }); MGT2.CreatureBehaviorSubType = Object.freeze({ accumulateur: "MGT2.CreatureBehaviorSubType.accumulateur", brouteur: "MGT2.CreatureBehaviorSubType.brouteur", filtreur: "MGT2.CreatureBehaviorSubType.filtreur", intermittent: "MGT2.CreatureBehaviorSubType.intermittent", chasseur: "MGT2.CreatureBehaviorSubType.chasseur", detourneur: "MGT2.CreatureBehaviorSubType.detourneux", guetteur: "MGT2.CreatureBehaviorSubType.guetteur", mangeur: "MGT2.CreatureBehaviorSubType.mangeur", piegeur: "MGT2.CreatureBehaviorSubType.piegeur", intimidateur: "MGT2.CreatureBehaviorSubType.intimidateur", necrophage: "MGT2.CreatureBehaviorSubType.necrophage", reducteur: "MGT2.CreatureBehaviorSubType.reducteur", opportuniste: "MGT2.CreatureBehaviorSubType.opportuniste" }); MGT2.HealingType = Object.freeze({ FIRST_AID: "MGT2.Healing.FirstAid", SURGERY: "MGT2.Healing.Surgery", MEDICAL_CARE: "MGT2.Healing.MedicalCare", NATURAL_HEALING: "MGT2.Healing.NaturalHealing" }); class ActorCharacter { static preCreate($this, data, options, user) { $this.updateSource({ prototypeToken: { actorLink: true } }); // QoL } static prepareData(actorData) { actorData.initiative = this.getInitiative(actorData); } static getInitiative($this) { let c = $this.system.config.initiative; return $this.system.characteristics[c].dm; } static async onDeleteDescendantDocuments($this, parent, collection, documents, ids, options, userId) { const toDeleteIds = []; const itemToUpdates = []; for (let d of documents) { if (d.type === "container") { // Delete content for (let item of $this.items) { if (item.system.hasOwnProperty("container") && item.system.container.id === d._id) toDeleteIds.push(item._id); } } else if (d.type === "computer") { // Eject software for (let item of $this.items) { if (item.system.hasOwnProperty("software") && item.system.computerId === d._id) { let clone = foundry.utils.deepClone(item); clone.system.software.computerId = ""; itemToUpdates.push(clone); } } } } if (toDeleteIds.length > 0) await $this.deleteEmbeddedDocuments("Item", toDeleteIds); if (itemToUpdates.length > 0) await $this.updateEmbeddedDocuments('Item', itemToUpdates); await this.recalculateWeight($this); } static async onUpdateDescendantDocuments($this, parent, collection, documents, changes, options, userId) { await this.calculEncumbranceAndWeight($this, parent, collection, documents, changes, options, userId); await this.calculComputers($this, parent, collection, documents, changes, options, userId); } static async calculComputers($this, parent, collection, documents, changes, options, userId) { let change; let i = 0; let recalculProcessing = false; for (let d of documents) { if (changes[i].hasOwnProperty("system")) { change = changes[i]; if (d.type === "item" && d.system.subType === "software") { if (change.system.software.hasOwnProperty("bandwidth") || change.system.software.hasOwnProperty("computerId")) { recalculProcessing = true; break; } } } } if (recalculProcessing) { let updatedComputers = []; let computerChanges = {}; let computers = []; for (let item of $this.items) { if (item.system.trash === true) continue; if (item.type === "computer") { computers.push(item); computerChanges[item._id] = { processingUsed: 0 }; } } for (let item of $this.items) { if (item.type !== "item" && item.system.subType !== "software") continue; if (item.system.software.hasOwnProperty("computerId") && item.system.software.computerId !== "") { computerChanges[item.system.software.computerId].processingUsed += item.system.software.bandwidth; } } for (let computer of computers) { let newProcessingUsed = computerChanges[computer._id].processingUsed; if (computer.system.processingUsed !== newProcessingUsed) { const cloneComputer = foundry.utils.deepClone($this.getEmbeddedDocument("Item", computer._id)); cloneComputer.system.processingUsed = newProcessingUsed; cloneComputer.system.overload = cloneComputer.system.processingUsed > cloneComputer.system.processing; updatedComputers.push(cloneComputer); } } if (updatedComputers.length > 0) { await $this.updateEmbeddedDocuments('Item', updatedComputers); } } } static async calculEncumbranceAndWeight($this, parent, collection, documents, changes, options, userId) { let recalculEncumbrance = false; let recalculWeight = false; let change; let i = 0; for (let d of documents) { if (changes[i].hasOwnProperty("system")) { change = changes[i]; if (d.type === "armor" || d.type === "computer" || d.type === "gear" || d.type === "item" || d.type === "weapon") { if (change.system.hasOwnProperty("quantity") || change.system.hasOwnProperty("weight") || change.system.hasOwnProperty("weightless") || change.system.hasOwnProperty("container") || change.system.hasOwnProperty("equipped") || d.type === "armor") { recalculWeight = true; } } else if (d.type === "talent" && d.system.subType === "skill") { if (change.system.level || (change.system?.hasOwnProperty("skill") && change.system?.skill.hasOwnProperty("reduceEncumbrance"))) { recalculEncumbrance = true; } } else if (d.type === "container" && (change.system.hasOwnProperty("onHand") || change.system.hasOwnProperty("weightless"))) { recalculWeight = true; } } i++; } if (recalculEncumbrance || recalculWeight) { const updateData = {}; this.recalculateArmor($this, updateData); if (recalculEncumbrance) { const str = $this.system.characteristics.strength.value; const end = $this.system.characteristics.endurance.value; let sumSkill = 0; $this.items.filter(x => x.type === "talent" && x.system.subType === "skill" && x.system.skill.reduceEncumbrance === true).forEach(x => sumSkill += x.system.level); let normal = str + end + sumSkill; let heavy = normal * 2; updateData["system.states.encumbrance"] = $this.system.inventory.weight > normal; updateData["system.inventory.encumbrance.normal"] = normal; updateData["system.inventory.encumbrance.heavy"] = heavy; } if (recalculWeight) await this.recalculateWeight($this, updateData); else if (Object.keys(updateData).length > 0) await $this.update(updateData); } } static recalculateArmor($this, updateData) { if (updateData === null || updateData === undefined) updateData = {}; let armor = 0; for (let item of $this.items) { if (item.type === "armor") { if (item.system.equipped === true && !isNaN(item.system.protection)) { armor += (+item.system.protection || 0); } } } updateData["system.inventory.armor"] = armor; return updateData; } static async recalculateWeight($this, updateData) { if (updateData === null || updateData === undefined) updateData = {}; let updatedContainers = []; let containerChanges = {}; let containers = []; // List all containers for (let item of $this.items) { if (item.system.trash === true) continue; if (item.type === "container") { containers.push(item); containerChanges[item._id] = { count: 0, weight: 0 }; } } let onHandWeight = 0; for (let item of $this.items) { if (item.type === "container") continue; if (item.system.hasOwnProperty("weightless") && item.system.weightless === true) continue; let itemWeight = 0; if (item.system.hasOwnProperty("weight")) { let itemQty = item.system.quantity; if (!isNaN(itemQty) && itemQty > 0) { itemWeight = item.system.weight; if (itemWeight > 0) { itemWeight *= itemQty; } } if (item.type === "armor") { if (item.system.equipped === true) { if (item.system.powered === true) itemWeight = 0; else itemWeight *= 0.25; // mass of armor that is being worn by 75% OPTIONAL } } if (item.system.container && item.system.container.id && item.system.container.id !== "") { // bad deleted container id if (containerChanges.hasOwnProperty(item.system.container.id)) { containerChanges[item.system.container.id].weight += Math.round(itemWeight * 10) / 10; containerChanges[item.system.container.id].count += item.system.quantity; } } else { onHandWeight += Math.round(itemWeight * 10) / 10; } } } // Check containers new weight for (let container of containers) { let newWeight = containerChanges[container._id].weight; let newCount = containerChanges[container._id].count; if (container.system.weight !== newWeight || container.system.count !== newCount) { updatedContainers.push({ _id: container._id, "system.weight": newWeight, "system.count": newCount, }); if (container.system.onHand === true && (container.system.weight > 0 || container.system.weightless !== true)) { onHandWeight += container.system.weight; } } } updateData["system.inventory.weight"] = onHandWeight; updateData["system.states.encumbrance"] = onHandWeight > $this.system.inventory.encumbrance.normal; await $this.update(updateData); if (updatedContainers.length > 0) { await $this.updateEmbeddedDocuments('Item', updatedContainers); } } static async preUpdate($this, changed, options, user) { // Calc encumbrance const newStr = foundry.utils.getProperty(changed, "system.characteristics.strength.value") ?? $this.system.characteristics.strength.value; const newEnd = foundry.utils.getProperty(changed, "system.characteristics.endurance.value") ?? $this.system.characteristics.endurance.value; if ((newStr !== $this.system.characteristics.strength.value) || (newEnd !== $this.system.characteristics.endurance.value)) { let sumSkill = 0; $this.items.filter(x => x.type === "talent" && x.system.subType === "skill" && x.system.skill.reduceEncumbrance === true).forEach(x => sumSkill += x.system.level); let normal = newStr + newEnd + sumSkill; let heavy = normal * 2; foundry.utils.setProperty(changed, "system.inventory.encumbrance.normal", normal); foundry.utils.setProperty(changed, "system.inventory.encumbrance.heavy", heavy); } //console.log(foundry.utils.getProperty(changed, "system.characteristics.strength.value")); const characteristicModified = this.computeCharacteristics(changed); const strengthValue = foundry.utils.getProperty(changed, "system.characteristics.strength.value") ?? $this.system.characteristics.strength.value; const strengthMax = foundry.utils.getProperty(changed, "system.characteristics.strength.max") ?? $this.system.characteristics.strength.max; const dexterityValue = foundry.utils.getProperty(changed, "system.characteristics.dexterity.value") ?? $this.system.characteristics.dexterity.value; const dexterityMax = foundry.utils.getProperty(changed, "system.characteristics.dexterity.max") ?? $this.system.characteristics.dexterity.max; const enduranceValue = foundry.utils.getProperty(changed, "system.characteristics.endurance.value") ?? $this.system.characteristics.endurance.value; const enduranceMax = foundry.utils.getProperty(changed, "system.characteristics.endurance.max") ?? $this.system.characteristics.endurance.max; const lifeValue = strengthValue + dexterityValue + enduranceValue; const lifeMax = strengthMax + dexterityMax + enduranceMax; if ($this.system.life.value !== lifeValue) foundry.utils.setProperty(changed, "system.life.value", lifeValue); if ($this.system.life.max !== lifeMax) foundry.utils.setProperty(changed, "system.life.max", lifeMax); if (characteristicModified && $this.system.personal.ucp === undefined || $this.system.personal.ucp === "") ; //} // Apply changes in Actor size to Token width/height // if ( "size" in (this.system.traits || {}) ) { // const newSize = foundry.utils.getProperty(changed, "system.traits.size"); // if ( newSize && (newSize !== this.system.traits?.size) ) { // let size = CONFIG.DND5E.tokenSizes[newSize]; // if ( !foundry.utils.hasProperty(changed, "prototypeToken.width") ) { // changed.prototypeToken ||= {}; // changed.prototypeToken.height = size; // changed.prototypeToken.width = size; // } // } // } } // static applyHealing($this, amount) { // if (isNaN(amount) || amount === 0) return; // const strength = $this.system.characteristics.strength; // const dexterity = $this.system.characteristics.dexterity; // const endurance = $this.system.characteristics.endurance; // const data = { // strength: { value: strength.value }, // dexterity: { value: dexterity.value }, // endurance: { value: endurance.value } // }; // $this.update({ system: { characteristics: data } }); // } static applyDamage($this, amount, { ignoreArmor = false } = {}) { if (isNaN(amount) || amount === 0) return; const rank1 = $this.system.config.damages.rank1; const rank2 = $this.system.config.damages.rank2; const rank3 = $this.system.config.damages.rank3; const data = {}; data[rank1] = { value: $this.system.characteristics[rank1].value }; data[rank2] = { value: $this.system.characteristics[rank2].value }; data[rank3] = { value: $this.system.characteristics[rank3].value }; if (amount < 0) amount = Math.abs(amount); if (!ignoreArmor) { const armorValue = $this.system.inventory?.armor ?? 0; amount = Math.max(0, amount - armorValue); if (amount === 0) return; } for (const [key, rank] of Object.entries(data)) { if (rank.value > 0) { if (rank.value >= amount) { rank.value -= amount; amount = 0; } else { amount -= rank.value; rank.value = 0; } rank.dm = this.getModifier(rank.value); if (amount <= 0) break; } } $this.update({ system: { characteristics: data } }); } static applyHealing($this, amount, type) { if (isNaN(amount) || amount === 0) return; const rank1 = $this.system.config.damages.rank1; const rank2 = $this.system.config.damages.rank2; const rank3 = $this.system.config.damages.rank3; // Data to restore (reverse cascade: END → DEX → STR) const data = {}; const rankOrder = [rank3, rank2, rank1]; // Reverse order for healing const maxValues = { [rank1]: $this.system.characteristics[rank1].max, [rank2]: $this.system.characteristics[rank2].max, [rank3]: $this.system.characteristics[rank3].max }; if (amount < 0) amount = Math.abs(amount); // Distribute healing from lowest rank first (END → DEX → STR typically) for (const rank of rankOrder) { const current = $this.system.characteristics[rank].value; const max = maxValues[rank]; if (current < max && amount > 0) { const canRestore = max - current; const restore = Math.min(amount, canRestore); if (!data[rank]) { data[rank] = { value: current }; } data[rank].value += restore; data[rank].dm = this.getModifier(data[rank].value); amount -= restore; } } // Only update if something was restored if (Object.keys(data).length > 0) { return $this.update({ system: { characteristics: data } }); } } static getContainers($this) { const containers = []; for (let item of $this.items) { if (item.type == "container") { containers.push(item); } } containers.sort(this.compareByName); return containers; } static getComputers($this) { const containers = []; for (let item of $this.items) { if (item.type == "computer") { containers.push(item); } } containers.sort(this.compareByName); return containers; } static getSkills($this) { const skills = []; for (let item of $this.items) { if (item.type === "talent" && item.system.subType === "skill") { skills.push(item); } } skills.sort(this.compareByName); return skills; } static computeCharacteristics(changed) { let modified = this.computeCharacteristic(changed, "strength"); if (this.computeCharacteristic(changed, "dexterity") && !modified) modified = true; if (this.computeCharacteristic(changed, "endurance") && !modified) modified = true; if (this.computeCharacteristic(changed, "intellect") && !modified) modified = true; if (this.computeCharacteristic(changed, "education") && !modified) modified = true; if (this.computeCharacteristic(changed, "social") && !modified) modified = true; if (this.computeCharacteristic(changed, "morale") && !modified) modified = true; if (this.computeCharacteristic(changed, "luck") && !modified) modified = true; if (this.computeCharacteristic(changed, "sanity") && !modified) modified = true; if (this.computeCharacteristic(changed, "charm") && !modified) modified = true; if (this.computeCharacteristic(changed, "psionic") && !modified) modified = true; if (this.computeCharacteristic(changed, "other") && !modified) modified = true; return modified; } static computeCharacteristic(changed, name) { //if (isNaN(c.value) || c.value <= 0) c.value = 0; //c.dm = this._getModifier(c.value) const path = `system.characteristics.${name}`; const newValue = foundry.utils.getProperty(changed, path + ".value");// || this.system.characteristics[name].value; if (newValue) { const dm = this.getModifier(newValue); foundry.utils.setProperty(changed, path + ".dm", dm); return true; } return false; } static getModifier(value) { if (isNaN(value) || value <= 0) return -3; if (value >= 1 && value <= 2) return -2; if (value >= 3 && value <= 5) return -1; if (value >= 6 && value <= 8) return 0; if (value >= 9 && value <= 11) return 1; if (value >= 12 && value <= 14) return 2; return 3; } static compareByName(a, b) { if (!a.hasOwnProperty("name") || !b.hasOwnProperty("name")) { return 0; } return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); } } class MGT2Combatant extends Combatant { } class TravellerActor extends Actor { prepareDerivedData() { if (this.type === "character") { this.system.initiative = ActorCharacter.getInitiative(this); } } async _preCreate(data, options, user) { if ( (await super._preCreate(data, options, user)) === false ) return false; if (this.type === "character") { ActorCharacter.preCreate(this, data, options, user); } } async _onDeleteDescendantDocuments(parent, collection, documents, ids, options, userId) { await super._onDeleteDescendantDocuments(parent, collection, documents, ids, options, userId); if (this.type === "character") { await ActorCharacter.onDeleteDescendantDocuments(this, parent, collection, documents, ids, options, userId); } } async _onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId) { super._onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId); //console.log("_onUpdateDescendantDocuments"); if (this.type === "character") { await ActorCharacter.onUpdateDescendantDocuments(this, parent, collection, documents, changes, options, userId); } } async _preUpdate(changed, options, user) { if ((await super._preUpdate(changed, options, user)) === false) return false; if (this.type === "character") { await ActorCharacter.preUpdate(this, changed, options, user); } } getInitiative($this) { if (this.type === "character") { return ActorCharacter.getInitiative(this); } } applyDamage(amount, { ignoreArmor = false } = {}) { if (this.type === "character") { return ActorCharacter.applyDamage(this, amount, { ignoreArmor }); } else if (this.type === "creature") { if (isNaN(amount) || amount === 0) return; if (amount < 0) amount = Math.abs(amount); const armorValue = ignoreArmor ? 0 : (this.system.armor ?? 0); const effective = Math.max(0, amount - armorValue); if (effective === 0) return; const newValue = Math.max(0, (this.system.life.value ?? 0) - effective); return this.update({ "system.life.value": newValue }); } } applyHealing(amount) { if (this.type === "character") { return ActorCharacter.applyHealing(this, amount); } else if (this.type === "creature") { if (isNaN(amount) || amount === 0) return; if (amount < 0) amount = Math.abs(amount); const maxValue = this.system.life.max ?? 0; const current = this.system.life.value ?? 0; const newValue = Math.min(current + amount, maxValue); if (newValue !== current) { return this.update({ "system.life.value": newValue }); } } } getContainers() { if (this.type === "character") { return ActorCharacter.getContainers(this); } return []; } getComputers() { if (this.type === "character") { return ActorCharacter.getComputers(this); } return []; } getSkills() { if (this.type === "character") { return ActorCharacter.getSkills(this); } return []; } async recalculateWeight() { if (this.type === "character") { return ActorCharacter.recalculateWeight(this); } } } class TravellerItem extends Item { /** @inheritdoc */ prepareDerivedData() { super.prepareDerivedData(); } async _preUpdate(changed, options, user) { if ((await super._preUpdate(changed, options, user)) === false) return false; if (this.type === "computer") { // Overload const newProcessing = foundry.utils.getProperty(changed, "system.processing") ?? this.system.processing; if (newProcessing !== this.system.processing) { let overload = this.system.processingUsed > newProcessing; foundry.utils.setProperty(changed, "system.overload", overload); } } // Qty max 1 if (this.type === "computer" || this.type === "container" || (this.type === "item" && this.system.subType === "software")) { const newQty = foundry.utils.getProperty(changed, "system.quantity") ?? this.system.quantity; if (newQty !== this.system.quantity && newQty > 1) { foundry.utils.setProperty(changed, "system.quantity", 1); } } // No Weight if (this.type === "item" && this.system.subType === "software") { const newWeight = foundry.utils.getProperty(changed, "system.weight") ?? this.system.weight; if (newWeight !== this.system.weight && newWeight > 0) { foundry.utils.setProperty(changed, "system.weight", 0); } } } getRollDisplay() { if (this.type === "talent") { if (this.system.subType === "skill") { let label; if (this.system.skill.speciality !== "" && this.system.skill.speciality !== undefined) { label = `${this.name} (${this.system.skill.speciality})`; } else { label = this.name; } if (this.system.level > 0) label += ` (+${this.system.level})`; else if (this.system.level < 0) label += ` (${this.system.level})`; return label; } else if (this.system.subType === "psionic") ; } return name; } } const { HandlebarsApplicationMixin: HandlebarsApplicationMixin$1 } = foundry.applications.api; class MGT2ActorSheet extends HandlebarsApplicationMixin$1(foundry.applications.sheets.ActorSheetV2) { static SHEET_MODES = { EDIT: 0, PLAY: 1 } constructor(options = {}) { super(options); this._sheetMode = this.constructor.SHEET_MODES.PLAY; } /** @override */ static DEFAULT_OPTIONS = { classes: ["mgt2", "sheet", "actor"], position: { width: 780, }, form: { submitOnChange: true, closeOnSubmit: false, }, window: { resizable: true, }, dragDrop: [{ dragSelector: ".drag-item-list", dropSelector: ".drop-item-list" }], actions: { toggleSheet: MGT2ActorSheet.#onToggleSheet, }, } get isPlayMode() { if (this._sheetMode === undefined) this._sheetMode = this.constructor.SHEET_MODES.PLAY; return this._sheetMode === this.constructor.SHEET_MODES.PLAY; } get isEditMode() { if (this._sheetMode === undefined) this._sheetMode = this.constructor.SHEET_MODES.PLAY; return this._sheetMode === this.constructor.SHEET_MODES.EDIT; } tabGroups = { sidebar: "health" } /** @override */ async _prepareContext() { const base = await super._prepareContext(); const actor = this.document; return { ...base, actor: actor, // Flat shorthands for template backward-compat (AppV1 style) name: actor.name, img: actor.img, cssClass: this.isEditable ? "editable" : "locked", system: actor.system, source: actor.toObject(), fields: actor.schema.fields, systemFields: actor.system.schema.fields, isEditable: this.isEditable, isEditMode: this.isEditMode, isPlayMode: this.isPlayMode, isGM: game.user.isGM, config: CONFIG.MGT2, }; } /** @override */ _onRender(context, options) { super._onRender(context, options); // Inject theme class dynamically (can't use game.settings in static DEFAULT_OPTIONS) const theme = game.settings.get("mgt2", "theme"); if (theme) this.element.classList.add(theme); this._activateTabGroups(); } _activateTabGroups() { for (const [group, activeTab] of Object.entries(this.tabGroups)) { const nav = this.element.querySelector(`nav[data-group="${group}"]`); if (!nav) continue; nav.querySelectorAll('[data-tab]').forEach(link => { link.classList.toggle('active', link.dataset.tab === activeTab); link.addEventListener('click', event => { event.preventDefault(); this.tabGroups[group] = link.dataset.tab; this.render(); }); }); this.element.querySelectorAll(`[data-group="${group}"][data-tab]`).forEach(content => { content.classList.toggle('active', content.dataset.tab === activeTab); }); } } /** @override */ _canDragDrop(selector) { return this.isEditable; } static async #onToggleSheet(event) { event.preventDefault(); this._sheetMode = this.isPlayMode ? this.constructor.SHEET_MODES.EDIT : this.constructor.SHEET_MODES.PLAY; this.render(); } } class MGT2Helper { static POUNDS_CONVERT = 2.20462262185; static decimalSeparator; static badDecimalSeparator; static { this.decimalSeparator = Number(1.1).toLocaleString().charAt(1); this.badDecimalSeparator = (this.decimalSeparator === "." ? "," : "."); } static format = function() { var s = arguments[0]; for (var i = 0; i < arguments.length - 1; i++) { var reg = new RegExp("\\{" + i + "\\}", "gm"); s = s.replace(reg, arguments[i + 1]); } return s; } static hasValue(object, property) { return object !== undefined && object.hasOwnProperty(property) && object[property] !== null && object[property] !== undefined && object[property] !== ""; } static getItemsWeight(items) { let weight = 0; for (let i of items) { let item = i.hasOwnProperty("system") ? i.system : i; if (item.hasOwnProperty("weightless") && item.weightless === true) { continue; } if (item.hasOwnProperty("weight")) { let itemQty = item.quantity; if (!isNaN(itemQty) && itemQty > 0) { let itemWeight = item.weight; if (itemWeight > 0) { weight += itemWeight * itemQty; } } } } return weight; } static generateUID() { let result = ''; const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < 36; i++) { const randomIndex = Math.floor(Math.random() * characters.length); result += characters.charAt(randomIndex); if (i === 8 || i === 12 || i === 16 || i === 20) result += "-"; } return result; } static compareByName(a, b) { if (!a.hasOwnProperty("name") || !b.hasOwnProperty("name")) { return 0; } return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); } static getDisplayDM(dm) { if (dm === 0) return " (0)"; if (dm > 0) return ` (+${dm})`; if (dm < 0) return ` (${dm})`; return ""; } static getFormulaDM(dm) { if (dm === 0) return "+0"; if (dm > 0) return `+${dm}`; if (dm < 0) return `${dm}`; return ""; } static getDiceResults(roll) { const results = []; for (const die of roll.dice) { results.push(die.results); } return results.flat(2); } static getDiceTotal(roll) { let total = 0; for (const die of roll.dice) { total += die.total; } return total; } static getDifficultyValue(difficulty) { switch(difficulty) { case "Simple": return 2; case "Easy": return 4; case "Routine": return 6; case "Average": return 8; case "Difficult": return 10; case "VeryDifficult": return 12; case "Formidable": return 14; case "Impossible": return 16; default: return 0; } } static getDifficultyDisplay(difficulty) { const key = `MGT2.Difficulty.${difficulty}`; const label = game.i18n.localize(key); return label !== key ? label : null; } static getRangeDisplay(range) { let value = Number(range.value); if (isNaN(value)) return null; let label; //if (game.settings.get("mgt2", "useDistanceMetric") === true) { if (range.unit !== null && range.unit !== undefined && range.unit !== "") label = game.i18n.localize(`MGT2.MetricRange.${range.unit}`).toLowerCase(); else label = ""; //} else { // TODO //} return `${value}${label}`; } static getWeightLabel() { //const label = game.settings.get("mgt2", "useWeightMetric") === true ? "MGT2.MetricSystem.Weight.kg" : "MGT2.ImperialSystem.Weight.lb"; //return game.i18n.localize(label); return game.i18n.localize("MGT2.MetricSystem.Weight.kg"); } static getDistanceLabel() { //const label = game.settings.get("mgt2", "useDistanceMetric") === true ? "MGT2.MetricSystem.Distance.km" : "MGT2.ImperialSystem.Distance.mi"; //return game.i18n.localize(label); return game.i18n.localize("MGT2.MetricSystem.Distance.km"); } static getIntegerFromInput(data) { return Math.trunc(this.getNumberFromInput(data)); } static getNumberFromInput(data) { if (data === undefined || data === null) return 0; if (typeof data === "string") { let converted = Number(data.replace(/\s+/g, '').replace(this.badDecimalSeparator, this.decimalSeparator).trim()); if (isNaN(converted)) return 0; return converted; } let converted = Number(data); if (isNaN(converted)) return 0; return converted; } static convertWeightForDisplay(weight) { //if (game.settings.get("mgt2", "useWeightMetric") === true || weight === 0) return weight; // Metric to Imperial //const pounds = weight * this.POUNDS_CONVERT; //return Math.round(pounds * 10) / 10; } static convertWeightFromInput(weight) { //if (game.settings.get("mgt2", "useWeightMetric") === true || weight === 0) return Math.round(weight * 10) / 10; // Imperial to Metric //const kg = this.POUNDS_CONVERT / weight; //return Math.round(kg * 10) / 10; } static getDataFromDropEvent(event) { try { return JSON.parse(event.dataTransfer?.getData("text/plain")); } catch (err) { return false; } //if ( data.type !== "Item" ) return false; //const item = await Item.implementation.fromDropData(data); } static async getItemDataFromDropData(dropData) { //console.log("getItemDataFromDropData"); let item; if (game.modules.get("monks-enhanced-journal")?.active && dropData.itemId && dropData.uuid.includes("JournalEntry")) { await fromUuid(dropData.uuid); } else if (dropData.hasOwnProperty("uuid")) { item = await fromUuid(dropData.uuid); } else { let uuid = `${dropData.type}.${dropData.data._id}`; item = await fromUuid(uuid); } if (!item) { throw new Error(game.i18n.localize("Errors.CouldNotFindItem").replace("_ITEM_ID_", dropData.uuid)); } if (item.pack) { const pack = game.packs.get(item.pack); item = await pack?.getDocument(item._id); } return deepClone(item); } } const { DialogV2: DialogV2$1 } = foundry.applications.api; const { renderTemplate: renderTemplate$3 } = foundry.applications.handlebars; const { FormDataExtended: FormDataExtended$1 } = foundry.applications.ux; class RollPromptHelper { static async roll(options) { // Backward compat: allow (actor, options) or just (options) if (options.rollTypeName || options.characteristics || options.skill !== undefined) ; else { // Called with (actor, options) options = arguments[1] || options; } const htmlContent = await renderTemplate$3('systems/mgt2/templates/roll-prompt.html', { config: CONFIG.MGT2, // Character-mode fields characteristics: options.characteristics ?? [], characteristic: options.characteristic ?? "", skills: options.skills ?? [], skill: options.skill ?? "", fatigue: options.fatigue ?? false, encumbrance: options.encumbrance ?? false, difficulty: options.difficulty ?? "Average", timeframe: options.timeframe ?? "Normal", customDM: options.customDM ?? "0", rollMode: options.rollMode ?? "publicroll", // Creature-mode flags isCreature: options.isCreature ?? false, creatureSkills: options.creatureSkills ?? [], selectedSkillIndex: options.selectedSkillIndex ?? -1, showSkillSelector: options.showSkillSelector ?? false, skillName: options.skillName ?? "", skillLevel: options.skillLevel ?? 0, // Healing fields showHeal: options.showHeal ?? false, healType: options.healType ?? null }); return await DialogV2$1.wait({ window: { title: options.title ?? options.rollTypeName ?? game.i18n.localize("MGT2.RollPrompt.Roll") }, classes: ["mgt2-roll-dialog"], content: htmlContent, rejectClose: false, buttons: [ { action: "boon", label: game.i18n.localize("MGT2.RollPrompt.Boon"), callback: (event, button, dialog) => { const formData = new FormDataExtended$1(dialog.element.querySelector('form')).object; formData.diceModifier = "dl"; return formData; } }, { action: "submit", label: game.i18n.localize("MGT2.RollPrompt.Roll"), icon: '', default: true, callback: (event, button, dialog) => { return new FormDataExtended$1(dialog.element.querySelector('form')).object; } }, { action: "bane", label: game.i18n.localize("MGT2.RollPrompt.Bane"), callback: (event, button, dialog) => { const formData = new FormDataExtended$1(dialog.element.querySelector('form')).object; formData.diceModifier = "dh"; return formData; } } ] }); } } const { DialogV2 } = foundry.applications.api; const { renderTemplate: renderTemplate$2 } = foundry.applications.handlebars; const { FormDataExtended } = foundry.applications.ux; async function _dialogWithForm(title, templatePath, templateData) { const htmlContent = await renderTemplate$2(templatePath, templateData); game.settings.get("mgt2", "theme"); return await DialogV2.wait({ window: { title }, content: htmlContent, rejectClose: false, buttons: [ { action: "submit", label: game.i18n.localize("MGT2.Save"), icon: '', default: true, callback: (event, button, dialog) => { return new FormDataExtended(dialog.element.querySelector('form')).object; } } ] }); } class CharacterPrompts { static async openConfig(system) { return _dialogWithForm( "Configuration", "systems/mgt2/templates/actors/actor-config-sheet.html", { config: CONFIG.MGT2, system } ); } static async openCharacteristic(name, show, showMax, showAll = false) { return _dialogWithForm( "Configuration: " + name, "systems/mgt2/templates/actors/actor-config-characteristic-sheet.html", { name, show, showMax, showAll } ); } static async openTraitEdit(data) { const title = data.name ?? game.i18n.localize("MGT2.Actor.EditTrait"); return _dialogWithForm( title, "systems/mgt2/templates/actors/trait-sheet.html", { config: CONFIG.MGT2, data } ); } static async openEditorFullView(title, html) { const htmlContent = await renderTemplate$2("systems/mgt2/templates/editor-fullview.html", { config: CONFIG.MGT2, html }); game.settings.get("mgt2", "theme"); await DialogV2.wait({ window: { title }, content: htmlContent, rejectClose: false, buttons: [ { action: "close", label: game.i18n.localize("MGT2.Close") || "Fermer", default: true, callback: () => null } ] }); } static async openHealingDays() { return await DialogV2.wait({ window: { title: game.i18n.localize("MGT2.Healing.Title") }, classes: ["mgt2-roll-dialog"], content: `
`, rejectClose: false, buttons: [ { action: "submit", label: game.i18n.localize("MGT2.Save"), icon: '', default: true, callback: (event, button, dialog) => { return new FormDataExtended(dialog.element.querySelector('form')).object; } } ] }); } } class TravellerCharacterSheet extends MGT2ActorSheet { /** @override */ static DEFAULT_OPTIONS = { ...super.DEFAULT_OPTIONS, classes: [...super.DEFAULT_OPTIONS.classes, "character", "nopad"], window: { ...super.DEFAULT_OPTIONS.window, title: "TYPES.Actor.character", }, actions: { ...super.DEFAULT_OPTIONS.actions, createItem: TravellerCharacterSheet.#onCreateItem, editItem: TravellerCharacterSheet.#onEditItem, deleteItem: TravellerCharacterSheet.#onDeleteItem, equipItem: TravellerCharacterSheet.#onEquipItem, itemStorageIn: TravellerCharacterSheet.#onItemStorageIn, itemStorageOut: TravellerCharacterSheet.#onItemStorageOut, softwareEject: TravellerCharacterSheet.#onSoftwareEject, createContainer: TravellerCharacterSheet.#onContainerCreate, editContainer: TravellerCharacterSheet.#onContainerEdit, deleteContainer: TravellerCharacterSheet.#onContainerDelete, roll: TravellerCharacterSheet.#onRoll, openConfig: TravellerCharacterSheet.#onOpenConfig, openCharacteristic: TravellerCharacterSheet.#onOpenCharacteristic, traitCreate: TravellerCharacterSheet.#onTraitCreate, traitEdit: TravellerCharacterSheet.#onTraitEdit, traitDelete: TravellerCharacterSheet.#onTraitDelete, openEditor: TravellerCharacterSheet.#onOpenEditor, heal: TravellerCharacterSheet.#onHeal, }, } /** @override */ static PARTS = { sheet: { template: "systems/mgt2/templates/actors/actor-sheet.html", }, } /** @override */ tabGroups = { sidebar: "inventory", characteristics: "core", inventory: "onhand", } /** @override */ async _prepareContext() { const context = await super._prepareContext(); const actor = this.document; const enrich = (html) => foundry.applications.ux.TextEditor.implementation.enrichHTML(html ?? "", { async: true }); context.enrichedBiography = await enrich(actor.system.biography); context.enrichedNotes = await enrich(actor.system.notes); context.enrichedFinanceNotes = await enrich(actor.system.finance?.notes); context.settings = { weightUnit: "kg", usePronouns: game.settings.get("mgt2", "usePronouns"), useGender: game.settings.get("mgt2", "useGender"), showLife: game.settings.get("mgt2", "showLife"), }; context.isGM = game.user.isGM; context.showTrash = false; context.initiative = actor.getInitiative(); this._prepareCharacterItems(context); return context; } _prepareCharacterItems(context) { const actor = this.document; const settings = context.settings; const items = actor.items; const weapons = [], armors = [], augments = [], computers = [], softwares = []; const miscItems = [], equipments = [], containerItems = [], careers = []; const skills = [], psionics = [], diseases = [], wounds = [], contacts = []; const actorContainers = []; for (let i of items) { if (i.type === "container") { actorContainers.push(i); } else if (i.type === "computer") { computers.push(i); i._subItems = []; if (i.system.overload === true) i._overloadClass = "computer-overload"; } } actorContainers.sort(MGT2Helper.compareByName); const containers = [{ name: "(tous)", _id: "" }].concat(actorContainers); const containerIndex = new Map(); for (let c of actorContainers) { containerIndex.set(c._id, c); if (c.system.weight > 0) { const w = MGT2Helper.convertWeightForDisplay(c.system.weight) + " " + settings.weightUnit; c._display = c.name.length > 12 ? `${c.name.substring(0, 12)}... (${w})` : `${c.name} (${w})`; } else { c._display = c.name.length > 12 ? c.name.substring(0, 12) + "..." : c.name; } if (c.system.onHand === true) c._subItems = []; } const containerView = actor.system.containerView; let currentContainerView = containerView !== "" ? containerIndex.get(containerView) : null; context.containerView = currentContainerView || null; context.containerWeight = currentContainerView ? MGT2Helper.convertWeightForDisplay(currentContainerView.system.weight) : MGT2Helper.convertWeightForDisplay(0); context.containerShowAll = containerView === ""; for (let i of items) { const item = i.system; if (item.hasOwnProperty("weight") && item.weight > 0) { i._weight = isNaN(item.quantity) ? MGT2Helper.convertWeightForDisplay(item.weight) + " " + settings.weightUnit : MGT2Helper.convertWeightForDisplay(item.weight * item.quantity) + " " + settings.weightUnit; } if (item.hasOwnProperty("container") && item.container.id !== "" && item.container.id !== undefined) { const container = containerIndex.get(item.container.id); if (container === undefined) { if (context.containerShowAll) { i._containerName = "#deleted#"; containerItems.push(i); } continue; } if (container.system.locked && !game.user.isGM) continue; if (container.system.onHand === true) container._subItems.push(i); if (context.containerShowAll || actor.system.containerView === item.container.id) { i._containerName = container.name; containerItems.push(i); } continue; } if (item.hasOwnProperty("equipped")) { i._canEquip = true; i.toggleClass = item.equipped ? "active" : ""; } else { i._canEquip = false; i.toggleClass = ""; } switch (i.type) { case "equipment": (i.system.subType === "augment" ? augments : equipments).push(i); break; case "armor": if (i.system.options?.length > 0) i._subInfo = i.system.options.map(x => x.name).join(", "); armors.push(i); break; case "computer": if (i.system.options?.length > 0) i._subInfo = i.system.options.map(x => x.name).join(", "); break; case "item": if (i.system.subType === "software") { if (i.system.software.computerId && i.system.software.computerId !== "") { const computer = computers.find(x => x._id === i.system.software.computerId); if (computer !== undefined) computer._subItems.push(i); else softwares.push(i); } else { i._display = i.system.software.bandwidth > 0 ? `${i.name} (${i.system.software.bandwidth})` : i.name; softwares.push(i); } } else { miscItems.push(i); } break; case "weapon": i._range = i.system.range.isMelee ? game.i18n.localize("MGT2.Melee") : MGT2Helper.getRangeDisplay(i.system.range); if (i.system.traits?.length > 0) i._subInfo = i.system.traits.map(x => x.name).join(", "); weapons.push(i); break; case "career": careers.push(i); break; case "contact": contacts.push(i); break; case "disease": (i.system.subType === "wound" ? wounds : diseases).push(i); break; case "talent": if (i.system.subType === "skill") { skills.push(i); } else { if (MGT2Helper.hasValue(i.system.psionic, "reach")) i._reach = game.i18n.localize(`MGT2.PsionicReach.${i.system.psionic.reach}`); if (MGT2Helper.hasValue(i.system.roll, "difficulty")) i._difficulty = game.i18n.localize(`MGT2.Difficulty.${i.system.roll.difficulty}`); psionics.push(i); } break; case "container": if (i.system.onHand === true) miscItems.push(i); break; } } const byName = MGT2Helper.compareByName; const byEquipName = (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()); context.encumbranceNormal = MGT2Helper.convertWeightForDisplay(actor.system.inventory.encumbrance.normal); context.encumbranceHeavy = MGT2Helper.convertWeightForDisplay(actor.system.inventory.encumbrance.heavy); const totalWeight = actor.system.inventory.weight; if (totalWeight > actor.system.inventory.encumbrance.heavy) { context.encumbranceClasses = "encumbrance-heavy"; context.encumbrance = 2; } else if (totalWeight > actor.system.inventory.encumbrance.normal) { context.encumbranceClasses = "encumbrance-normal"; context.encumbrance = 1; } else { context.encumbrance = 0; } if (softwares.length > 0) { softwares.sort(byName); context.softwares = softwares; } augments.sort(byEquipName); context.augments = augments; armors.sort(byEquipName); context.armors = armors; computers.sort(byEquipName); context.computers = computers; context.careers = careers; contacts.sort(byName); context.contacts = contacts; containers.sort(byName); context.containers = containers; diseases.sort(byName); context.diseases = diseases; context.wounds = wounds; equipments.sort(byEquipName); context.equipments = equipments; miscItems.sort(byEquipName); context.items = miscItems; actorContainers.sort(byName); context.actorContainers = actorContainers; skills.sort(byName); context.skills = skills; psionics.sort(byName); context.psionics = psionics; weapons.sort(byEquipName); context.weapons = weapons; if (containerItems.length > 0) { containerItems.sort((a, b) => { const r = a._containerName.localeCompare(b._containerName); return r !== 0 ? r : a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); } context.containerItems = containerItems; } // ========================================================= // Event Binding (AppV2 _onRender — replaces jQuery activateListeners) // Templates still use CSS class selectors, so we bind manually here. // ========================================================= /** @override */ _onRender(context, options) { super._onRender(context, options); const html = this.element; if (!this.isEditable) return; this._bindClassEvent(html, "a[data-roll]", "click", TravellerCharacterSheet.#onRoll); this._bindClassEvent(html, ".cfg-characteristic", "click", TravellerCharacterSheet.#onOpenCharacteristic); this._bindClassEvent(html, ".item-create", "click", TravellerCharacterSheet.#onCreateItem); this._bindClassEvent(html, ".item-edit", "click", TravellerCharacterSheet.#onEditItem); this._bindClassEvent(html, ".item-delete", "click", TravellerCharacterSheet.#onDeleteItem); this._bindClassEvent(html, ".item-storage-in", "click", TravellerCharacterSheet.#onItemStorageIn); this._bindClassEvent(html, ".item-storage-out", "click", TravellerCharacterSheet.#onItemStorageOut); this._bindClassEvent(html, ".software-eject", "click", TravellerCharacterSheet.#onSoftwareEject); this._bindClassEvent(html, ".container-create", "click", TravellerCharacterSheet.#onContainerCreate); this._bindClassEvent(html, ".container-edit", "click", TravellerCharacterSheet.#onContainerEdit); this._bindClassEvent(html, ".container-delete", "click", TravellerCharacterSheet.#onContainerDelete); this._bindClassEvent(html, ".traits-create", "click", TravellerCharacterSheet.#onTraitCreate); this._bindClassEvent(html, ".traits-edit", "click", TravellerCharacterSheet.#onTraitEdit); this._bindClassEvent(html, ".traits-delete", "click", TravellerCharacterSheet.#onTraitDelete); this._bindClassEvent(html, "[data-editor='open']", "click", TravellerCharacterSheet.#onOpenEditor); html.querySelector("[name='config']")?.addEventListener("click", (ev) => TravellerCharacterSheet.#onOpenConfig.call(this, ev, ev.currentTarget)); } /** Helper: bind a handler to all matching elements, with `this` set to the sheet instance */ _bindClassEvent(html, selector, event, handler) { for (const el of html.querySelectorAll(selector)) { el.addEventListener(event, (ev) => handler.call(this, ev, ev.currentTarget)); } } // ========================================================= // Drag & Drop // ========================================================= /** @override */ async _onDrop(event) { event.preventDefault(); event.stopImmediatePropagation(); const dropData = MGT2Helper.getDataFromDropEvent(event); if (!dropData) return false; const sourceItemData = await MGT2Helper.getItemDataFromDropData(dropData); if (sourceItemData.type === "species") { const update = { system: { personal: { species: sourceItemData.name, speciesText: { description: sourceItemData.system.description, descriptionLong: sourceItemData.system.descriptionLong, }, }, }, }; update.system.personal.traits = this.actor.system.personal.traits.concat(sourceItemData.system.traits); if (sourceItemData.system.modifiers?.length > 0) { update.system.characteristics = {}; for (let modifier of sourceItemData.system.modifiers) { if (MGT2Helper.hasValue(modifier, "characteristic") && MGT2Helper.hasValue(modifier, "value")) { const c = this.actor.system.characteristics[modifier.characteristic]; const updateValue = { value: c.value + modifier.value }; if (c.showMax) updateValue.max = c.max + modifier.value; update.system.characteristics[modifier.characteristic] = updateValue; } } } this.actor.update(update); return true; } if (["contact", "disease", "career", "talent"].includes(sourceItemData.type)) { let transferData = {}; try { transferData = sourceItemData.toJSON(); } catch (e) { transferData = sourceItemData; } delete transferData._id; delete transferData.id; await this.actor.createEmbeddedDocuments("Item", [transferData]); return true; } if (!["armor", "weapon", "computer", "container", "item", "equipment"].includes(sourceItemData.type)) return false; const target = event.target.closest(".table-row"); let targetId = null; let targetItem = null; if (target !== null) { targetId = target.dataset.itemId; targetItem = this.actor.getEmbeddedDocument("Item", targetId); } let sourceItem = this.actor.getEmbeddedDocument("Item", sourceItemData.id); if (sourceItem) { if (!targetItem) return false; if (sourceItem.id === targetId) return false; if (targetItem.type === "item" || targetItem.type === "equipment") { if (targetItem.system.subType === "software") await sourceItem.update({ "system.software.computerId": targetItem.system.software.computerId }); else await sourceItem.update({ "system.container.id": targetItem.system.container.id }); return true; } else if (targetItem.type === "computer") { await sourceItem.update({ "system.software.computerId": targetId }); return true; } else if (targetItem.type === "container") { if (targetItem.system.locked && !game.user.isGM) { ui.notifications.error("Verrouillé"); } else { await sourceItem.update({ "system.container.id": targetId }); return true; } } } else { let transferData = {}; try { transferData = sourceItemData.toJSON(); } catch (e) { transferData = sourceItemData; } delete transferData._id; delete transferData.id; const recalcWeight = transferData.system.hasOwnProperty("weight"); if (transferData.system.hasOwnProperty("container")) transferData.system.container.id = ""; if (transferData.type === "item" && transferData.system.subType === "software") transferData.system.software.computerId = ""; if (transferData.type === "container") transferData.system.onHand = true; if (transferData.system.hasOwnProperty("equipment")) transferData.system.equipped = false; if (targetItem !== null) { if (transferData.type === "item" && transferData.system.subType === "software") { if (targetItem.type === "item" && targetItem.system.subType === "software") transferData.system.software.computerId = targetItem.system.software.computerId; else if (targetItem.type === "computer") transferData.system.software.computerId = targetItem._id; } else if (["armor", "computer", "equipment", "item", "weapon"].includes(transferData.type)) { if (targetItem.type === "container") { if (!targetItem.system.locked || game.user.isGM) transferData.system.container.id = targetId; } else { transferData.system.container.id = targetItem.system.container.id; } } } await this.actor.createEmbeddedDocuments("Item", [transferData]); if (recalcWeight) await this.actor.recalculateWeight(); } return true; } // ========================================================= // Actions (static private methods) // ========================================================= static async #onCreateItem(event, target) { event.preventDefault(); const data = { name: target.dataset.createName, type: target.dataset.typeItem, }; if (target.dataset.subtype) { data.system = { subType: target.dataset.subtype }; } const cls = getDocumentClass("Item"); return cls.create(data, { parent: this.actor }); } static async #onEditItem(event, target) { event.preventDefault(); const li = target.closest("[data-item-id]"); const item = this.actor.getEmbeddedDocument("Item", li?.dataset.itemId); if (item) item.sheet.render(true); } static async #confirmDelete(name) { return foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.localize("MGT2.Dialog.ConfirmDeleteTitle") }, content: `${game.i18n.format("MGT2.Dialog.ConfirmDeleteContent", { name })}
`, yes: { label: game.i18n.localize("MGT2.Dialog.Yes"), icon: "fas fa-trash" }, no: { label: game.i18n.localize("MGT2.Dialog.No"), icon: "fas fa-times" }, rejectClose: false, modal: true }); } static async #onDeleteItem(event, target) { event.preventDefault(); const li = target.closest("[data-item-id]"); if (!li?.dataset.itemId) return; const item = this.actor.items.get(li.dataset.itemId); if (!item) return; const confirmed = await TravellerCharacterSheet.#confirmDelete(item.name); if (!confirmed) return; this.actor.deleteEmbeddedDocuments("Item", [li.dataset.itemId]); } static async #onEquipItem(event, target) { event.preventDefault(); const li = target.closest("[data-item-id]"); const itemId = li?.dataset.itemId; if (!itemId) return; const item = this.actor.items.get(itemId); if (!item) return; await item.update({ "system.equipped": !item.system.equipped }); } static async #onItemStorageIn(event, target) { event.preventDefault(); const li = target.closest("[data-item-id]"); const item = this.actor.getEmbeddedDocument("Item", li?.dataset.itemId); if (!item) return; if (item.type === "container") { await item.update({ "system.onHand": false }); } else { const containers = this.actor.getContainers(); let container; const dropInId = this.actor.system.containerDropIn; if (!dropInId) { container = containers.length === 0 ? await getDocumentClass("Item").create({ name: "New container", type: "container" }, { parent: this.actor }) : containers[0]; } else { container = containers.find(x => x._id === dropInId); } if (container?.system.locked && !game.user.isGM) { ui.notifications.error("Objet verrouillé"); return; } await item.update({ "system.container.id": container._id }); } } static async #onItemStorageOut(event, target) { event.preventDefault(); const li = target.closest("[data-item-id]"); const item = this.actor.getEmbeddedDocument("Item", li?.dataset.itemId); if (!item) return; await item.update({ "system.container.id": "" }); } static async #onSoftwareEject(event, target) { event.preventDefault(); const li = target.closest("[data-item-id]"); const item = this.actor.getEmbeddedDocument("Item", li?.dataset.itemId); if (!item) return; await item.update({ "system.software.computerId": "" }); } static async #onContainerCreate(event) { event.preventDefault(); const cls = getDocumentClass("Item"); return cls.create({ name: "New container", type: "container" }, { parent: this.actor }); } static async #onContainerEdit(event) { event.preventDefault(); const container = this.actor.getEmbeddedDocument("Item", this.actor.system.containerView); if (container) container.sheet.render(true); } static async #onContainerDelete(event) { event.preventDefault(); const containers = this.actor.getContainers(); const container = containers.find(x => x._id === this.actor.system.containerView); if (!container) return; const confirmed = await TravellerCharacterSheet.#confirmDelete(container.name); if (!confirmed) return; const containerItems = this.actor.items.filter( x => x.system.hasOwnProperty("container") && x.system.container.id === container._id ); if (containerItems.length > 0) { const updates = containerItems.map(item => ({ _id: item.id, "system.container.id": "" })); await this.actor.updateEmbeddedDocuments("Item", updates); } const actorUpdate = { "system.containerView": "" }; if (this.actor.system.containerDropIn === container._id) { actorUpdate["system.containerDropIn"] = ""; const remaining = containers.filter(x => x._id !== container._id); if (remaining.length > 0) actorUpdate["system.containerDropIn"] = remaining[0]._id; } await this.actor.deleteEmbeddedDocuments("Item", [container._id]); await this.actor.update(actorUpdate); } static async #onRoll(event, target) { event.preventDefault(); const rollOptions = { rollTypeName: game.i18n.localize("MGT2.RollPrompt.Roll"), rollObjectName: "", characteristics: [{ _id: "", name: "" }], characteristic: "", skills: [], skill: "", fatigue: this.actor.system.states.fatigue, encumbrance: this.actor.system.states.encumbrance, difficulty: null, damageFormula: null, isMelee: false, }; const cardButtons = []; for (const [key, label] of Object.entries(MGT2.Characteristics)) { const c = this.actor.system.characteristics[key]; if (c.show) { rollOptions.characteristics.push({ _id: key, name: game.i18n.localize(label) + MGT2Helper.getDisplayDM(c.dm), }); } } for (let item of this.actor.items) { if (item.type === "talent" && item.system.subType === "skill") rollOptions.skills.push({ _id: item._id, name: item.getRollDisplay() }); } rollOptions.skills.sort(MGT2Helper.compareByName); rollOptions.skills = [{ _id: "NP", name: game.i18n.localize("MGT2.Items.NotProficient") }].concat(rollOptions.skills); let itemObj = null; let isInitiative = false; const rollType = target.dataset.roll; if (rollType === "initiative") { rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.InitiativeRoll"); rollOptions.characteristic = this.actor.system.config.initiative; isInitiative = true; } else if (rollType === "characteristic") { rollOptions.characteristic = target.dataset.rollCharacteristic; rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.CharacteristicRoll"); rollOptions.rollObjectName = game.i18n.localize(`MGT2.Characteristics.${rollOptions.characteristic}.name`); } else { if (rollType === "skill") { rollOptions.skill = target.dataset.rollSkill; itemObj = this.actor.getEmbeddedDocument("Item", rollOptions.skill); rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.SkillRoll"); rollOptions.rollObjectName = itemObj.name; } else if (rollType === "psionic") { rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.PsionicRoll"); } if (itemObj === null && target.dataset.itemId) { itemObj = this.actor.getEmbeddedDocument("Item", target.dataset.itemId); rollOptions.rollObjectName = itemObj.name; if (itemObj.type === "weapon") rollOptions.rollTypeName = game.i18n.localize("TYPES.Item.weapon"); else if (itemObj.type === "armor") rollOptions.rollTypeName = game.i18n.localize("TYPES.Item.armor"); else if (itemObj.type === "computer") rollOptions.rollTypeName = game.i18n.localize("TYPES.Item.computer"); } if (rollType === "psionic" && itemObj) { rollOptions.rollObjectName = itemObj.name; if (MGT2Helper.hasValue(itemObj.system.psionic, "duration")) { cardButtons.push({ label: game.i18n.localize("MGT2.Items.Duration"), formula: itemObj.system.psionic.duration, message: { objectName: itemObj.name, flavor: "{0} ".concat(game.i18n.localize(`MGT2.Durations.${itemObj.system.psionic.durationUnit}`)), }, }); } } if (itemObj?.system.hasOwnProperty("damage")) { rollOptions.damageFormula = itemObj.system.damage; if (itemObj.type === "weapon") { rollOptions.isMelee = itemObj.system.range?.isMelee === true; } if (itemObj.type === "disease") { if (itemObj.system.subType === "disease") rollOptions.rollTypeName = game.i18n.localize("MGT2.DiseaseSubType.disease"); else if (itemObj.system.subType === "poison") rollOptions.rollTypeName = game.i18n.localize("MGT2.DiseaseSubType.poison"); } } if (itemObj?.system.hasOwnProperty("roll")) { if (MGT2Helper.hasValue(itemObj.system.roll, "characteristic")) rollOptions.characteristic = itemObj.system.roll.characteristic; if (MGT2Helper.hasValue(itemObj.system.roll, "skill")) rollOptions.skill = itemObj.system.roll.skill; if (MGT2Helper.hasValue(itemObj.system.roll, "difficulty")) rollOptions.difficulty = itemObj.system.roll.difficulty; } } const userRollData = await RollPromptHelper.roll(rollOptions); const rollModifiers = []; const rollFormulaParts = []; if (userRollData.diceModifier) { rollFormulaParts.push("3d6", userRollData.diceModifier); } else { rollFormulaParts.push("2d6"); } if (userRollData.characteristic) { const c = this.actor.system.characteristics[userRollData.characteristic]; rollFormulaParts.push(MGT2Helper.getFormulaDM(c.dm)); rollModifiers.push(game.i18n.localize(`MGT2.Characteristics.${userRollData.characteristic}.name`) + MGT2Helper.getDisplayDM(c.dm)); } if (userRollData.skill) { if (userRollData.skill === "NP") { rollFormulaParts.push("-3"); rollModifiers.push(game.i18n.localize("MGT2.Items.NotProficient")); } else { const skillObj = this.actor.getEmbeddedDocument("Item", userRollData.skill); rollFormulaParts.push(MGT2Helper.getFormulaDM(skillObj.system.level)); rollModifiers.push(skillObj.getRollDisplay()); } } if (userRollData.psionic) { const psionicObj = this.actor.getEmbeddedDocument("Item", userRollData.psionic); rollFormulaParts.push(MGT2Helper.getFormulaDM(psionicObj.system.level)); rollModifiers.push(psionicObj.getRollDisplay()); } if (userRollData.timeframes && userRollData.timeframes !== "" && userRollData.timeframes !== "Normal") { rollModifiers.push(game.i18n.localize(`MGT2.Timeframes.${userRollData.timeframes}`)); rollFormulaParts.push(userRollData.timeframes === "Slower" ? "+2" : "-2"); } if (userRollData.encumbrance === true) { rollFormulaParts.push("-2"); rollModifiers.push(game.i18n.localize("MGT2.Actor.Encumbrance") + " -2"); } if (userRollData.fatigue === true) { rollFormulaParts.push("-2"); rollModifiers.push(game.i18n.localize("MGT2.Actor.Fatigue") + " -2"); } const customDMVal = parseInt(userRollData.customDM ?? "0", 10); if (!isNaN(customDMVal) && customDMVal !== 0) { rollFormulaParts.push(customDMVal > 0 ? `+${customDMVal}` : `${customDMVal}`); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.CustomDM") + " " + (customDMVal > 0 ? `+${customDMVal}` : `${customDMVal}`)); } if (MGT2Helper.hasValue(userRollData, "difficulty") && userRollData.difficulty !== "") rollOptions.difficulty = userRollData.difficulty; const rollFormula = rollFormulaParts.join(""); if (!Roll.validate(rollFormula)) { ui.notifications.error(game.i18n.localize("MGT2.Errors.InvalidRollFormula")); return; } let roll = await new Roll(rollFormula, this.actor.getRollData()).roll({ rollMode: userRollData.rollMode }); if (isInitiative && this.token?.combatant) { await this.token.combatant.update({ initiative: roll.total }); } // ── Compute effect and effective damage formula ────────────────────── let rollSuccess = false; let rollFailure = false; let rollEffect = undefined; let rollEffectStr = undefined; let difficultyValue = null; if (MGT2Helper.hasValue(rollOptions, "difficulty")) { difficultyValue = MGT2Helper.getDifficultyValue(rollOptions.difficulty); rollEffect = roll.total - difficultyValue; rollEffectStr = (rollEffect >= 0 ? "+" : "") + rollEffect; rollSuccess = rollEffect >= 0; rollFailure = !rollSuccess; } // Build effective damage formula: base + effect + STR DM (melee) let effectiveDamageFormula = rollOptions.damageFormula || null; if (effectiveDamageFormula) { if (rollEffect !== undefined && rollEffect !== 0) { effectiveDamageFormula += (rollEffect >= 0 ? "+" : "") + rollEffect; } if (rollOptions.isMelee) { const strDm = this.actor.system.characteristics.strength?.dm ?? 0; if (strDm !== 0) effectiveDamageFormula += (strDm >= 0 ? "+" : "") + strDm; } } // ── Build roll breakdown tooltip ───────────────────────────────────── const diceRawTotal = roll.dice.reduce((s, d) => s + d.total, 0); const breakdownParts = [game.i18n.localize("MGT2.Chat.Roll.Dice") + " " + diceRawTotal]; for (const mod of rollModifiers) breakdownParts.push(mod); if (rollEffectStr !== undefined) breakdownParts.push(game.i18n.localize("MGT2.Chat.Roll.Effect") + " " + rollEffectStr); const rollBreakdown = breakdownParts.join(" | "); const chatData = { user: game.user.id, speaker: this.actor ? ChatMessage.getSpeaker({ actor: this.actor }) : null, formula: roll._formula, tooltip: await roll.getTooltip(), total: Math.round(roll.total * 100) / 100, rollBreakdown, showButtons: true, showLifeButtons: false, showRollRequest: false, rollTypeName: rollOptions.rollTypeName, rollObjectName: rollOptions.rollObjectName, rollModifiers: rollModifiers, // Show damage button only if there's a formula AND (no difficulty check OR roll succeeded) showRollDamage: !!effectiveDamageFormula && (!difficultyValue || rollSuccess), cardButtons: cardButtons, }; if (MGT2Helper.hasValue(rollOptions, "difficulty")) { chatData.rollDifficulty = rollOptions.difficulty; chatData.rollDifficultyLabel = MGT2Helper.getDifficultyDisplay(rollOptions.difficulty); chatData.rollEffect = rollEffect; chatData.rollEffectStr = rollEffectStr; chatData.rollSuccess = rollSuccess || undefined; chatData.rollFailure = rollFailure || undefined; } const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData); chatData.content = html; let flags = null; if (effectiveDamageFormula) { flags = { mgt2: { damage: { formula: effectiveDamageFormula, rollObjectName: rollOptions.rollObjectName, rollTypeName: rollOptions.rollTypeName } } }; } if (cardButtons.length > 0) { if (!flags) flags = { mgt2: {} }; flags.mgt2.buttons = cardButtons; } if (flags) chatData.flags = flags; return roll.toMessage(chatData); } static async #onOpenConfig(event) { event.preventDefault(); const userConfig = await CharacterPrompts.openConfig(this.actor.system); if (userConfig) this.actor.update({ "system.config": userConfig }); } static async #onOpenCharacteristic(event, target) { event.preventDefault(); const name = target.dataset.cfgCharacteristic; const c = this.actor.system.characteristics[name]; let showAll = false; for (const value of Object.values(this.actor.system.characteristics)) { if (!value.show) { showAll = true; break; } } const userConfig = await CharacterPrompts.openCharacteristic( game.i18n.localize(`MGT2.Characteristics.${name}.name`), c.show, c.showMax, showAll ); if (userConfig) { const data = { system: { characteristics: {} } }; data.system.characteristics[name] = { show: userConfig.show, showMax: userConfig.showMax }; if (userConfig.showAll === true) { for (const [key, value] of Object.entries(this.actor.system.characteristics)) { if (key !== name && !value.show) data.system.characteristics[key] = { show: true }; } } this.actor.update(data); } } static async #onTraitCreate(event) { event.preventDefault(); let traits = this.actor.system.personal.traits; let newTraits; if (traits.length === 0) { newTraits = [{ name: "", description: "" }]; } else { newTraits = [...traits, { name: "", description: "" }]; } return this.actor.update({ system: { personal: { traits: newTraits } } }); } static async #onTraitEdit(event, target) { event.preventDefault(); const element = target.closest("[data-traits-part]"); const index = Number(element.dataset.traitsPart); const trait = this.actor.system.personal.traits[index]; const result = await CharacterPrompts.openTraitEdit(trait); const traits = [...this.actor.system.personal.traits]; traits[index] = { ...traits[index], name: result.name, description: result.description }; return this.actor.update({ system: { personal: { traits: traits } } }); } static async #onTraitDelete(event, target) { event.preventDefault(); const confirmed = await TravellerCharacterSheet.#confirmDelete(game.i18n.localize("MGT2.Actor.ThisTrait")); if (!confirmed) return; const element = target.closest("[data-traits-part]"); const index = Number(element.dataset.traitsPart); const traits = foundry.utils.deepClone(this.actor.system.personal.traits); const newTraits = Object.entries(traits) .filter(([key]) => Number(key) !== index) .map(([, value]) => value); return this.actor.update({ system: { personal: { traits: newTraits } } }); } static async #onOpenEditor(event) { event.preventDefault(); await CharacterPrompts.openEditorFullView( this.actor.system.personal.species, this.actor.system.personal.speciesText.descriptionLong ); } static async #onHeal(event, target) { event.preventDefault(); const healType = target.dataset.healType; if (canvas.tokens.controlled.length === 0) { ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected")); return; } if (healType === "firstaid") { // Find Medicine skill to pre-select // Use normalized string matching to handle accents const medSkill = this.actor.items.find(i => { if (i.type !== "talent" || i.system.subType !== "skill") return false; const normalized = i.name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); return normalized.includes("medecin") || normalized.includes("medicine"); }); // Only EDU characteristic available for First Aid const characteristics = [ { _id: "education", name: game.i18n.localize("MGT2.Characteristics.education.name") } ]; const rollOptions = { rollTypeName: game.i18n.localize("MGT2.Healing.FirstAid"), rollObjectName: this.actor.name, characteristics: characteristics, // Only EDU characteristic: "education", // Pre-selected skill: medSkill?.id ?? "", // Medicine skill ID for pre-selection (must match _id in array) skillName: medSkill?.name ?? game.i18n.localize("MGT2.Healing.NoMedicineSkill"), // Display name skillLevel: medSkill?.system.level ?? -3, // -3 if not found skills: medSkill ? [{ _id: medSkill.id, name: medSkill.name, level: medSkill.system.level }] : [], difficulty: "Average", // First Aid difficulty is 8 (Average) showHeal: true, healType: MGT2.HealingType.FIRST_AID, }; const userRollData = await RollPromptHelper.roll(rollOptions); if (userRollData) { // Build formula with all DMs — same pattern as standard skill roll const rollFormulaParts = []; const rollModifiers = []; if (userRollData.diceModifier) { rollFormulaParts.push("3d6"); rollFormulaParts.push(userRollData.diceModifier); } else { rollFormulaParts.push("2d6"); } if (userRollData.characteristic) { const c = this.actor.system.characteristics[userRollData.characteristic]; rollFormulaParts.push(MGT2Helper.getFormulaDM(c.dm)); rollModifiers.push(game.i18n.localize(`MGT2.Characteristics.${userRollData.characteristic}.name`) + MGT2Helper.getDisplayDM(c.dm)); } if (userRollData.skill && userRollData.skill !== "") { if (userRollData.skill === "NP") { rollFormulaParts.push("-3"); rollModifiers.push(game.i18n.localize("MGT2.Items.NotProficient")); } else { const skillObj = this.actor.getEmbeddedDocument("Item", userRollData.skill); rollFormulaParts.push(MGT2Helper.getFormulaDM(skillObj.system.level)); rollModifiers.push(skillObj.getRollDisplay()); } } if (userRollData.customDM && userRollData.customDM !== "") { let s = userRollData.customDM.trim(); if (/^[0-9]/.test(s)) rollFormulaParts.push("+"); rollFormulaParts.push(s); rollModifiers.push("DM " + s); } const rollFormula = rollFormulaParts.join(""); const roll = await new Roll(rollFormula, this.actor.getRollData()).roll(); // Difficulty for First Aid is Average (8) const difficulty = 8; const effect = roll.total - difficulty; const isSuccess = effect >= 0; const healing = isSuccess ? Math.max(1, effect) : 0; const cardButtons = isSuccess ? [{ label: game.i18n.localize("MGT2.Healing.ApplyHealing"), action: "healing" }] : []; const chatData = { user: game.user.id, speaker: ChatMessage.getSpeaker({ actor: this.actor }), formula: roll._formula, tooltip: await roll.getTooltip(), total: Math.round(roll.total * 100) / 100, rollTypeName: game.i18n.localize("MGT2.Healing.FirstAid"), rollObjectName: this.actor.name, rollModifiers: rollModifiers, rollDifficulty: difficulty, rollDifficultyLabel: MGT2Helper.getDifficultyDisplay("Average"), rollEffectStr: isSuccess ? effect.toString() : undefined, healingAmount: isSuccess ? healing : undefined, rollSuccess: isSuccess || undefined, rollFailure: !isSuccess || undefined, showButtons: isSuccess, hasDamage: false, showRollDamage: false, cardButtons: cardButtons, }; const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData); chatData.content = html; chatData.flags = { mgt2: { healing: { amount: healing } } }; return roll.toMessage(chatData); } } else if (healType === "surgery") { // Find Medicine skill to pre-select (same as first aid) const medSkill = this.actor.items.find(i => { if (i.type !== "talent" || i.system.subType !== "skill") return false; const normalized = i.name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); return normalized.includes("medecin") || normalized.includes("medicine"); }); const characteristics = [ { _id: "education", name: game.i18n.localize("MGT2.Characteristics.education.name") } ]; const rollOptions = { rollTypeName: game.i18n.localize("MGT2.Healing.Surgery"), rollObjectName: this.actor.name, characteristics: characteristics, characteristic: "education", skill: medSkill?.id ?? "", skillName: medSkill?.name ?? game.i18n.localize("MGT2.Healing.NoMedicineSkill"), skillLevel: medSkill?.system.level ?? -3, skills: medSkill ? [{ _id: medSkill.id, name: medSkill.name, level: medSkill.system.level }] : [], difficulty: "Average", showHeal: true, healType: MGT2.HealingType.SURGERY, }; const userRollData = await RollPromptHelper.roll(rollOptions); if (userRollData) { // Build formula with all DMs — same pattern as standard skill roll const rollFormulaParts = []; const rollModifiers = []; if (userRollData.diceModifier) { rollFormulaParts.push("3d6"); rollFormulaParts.push(userRollData.diceModifier); } else { rollFormulaParts.push("2d6"); } if (userRollData.characteristic) { const c = this.actor.system.characteristics[userRollData.characteristic]; rollFormulaParts.push(MGT2Helper.getFormulaDM(c.dm)); rollModifiers.push(game.i18n.localize(`MGT2.Characteristics.${userRollData.characteristic}.name`) + MGT2Helper.getDisplayDM(c.dm)); } if (userRollData.skill && userRollData.skill !== "") { if (userRollData.skill === "NP") { rollFormulaParts.push("-3"); rollModifiers.push(game.i18n.localize("MGT2.Items.NotProficient")); } else { const skillObj = this.actor.getEmbeddedDocument("Item", userRollData.skill); rollFormulaParts.push(MGT2Helper.getFormulaDM(skillObj.system.level)); rollModifiers.push(skillObj.getRollDisplay()); } } if (userRollData.customDM && userRollData.customDM !== "") { let s = userRollData.customDM.trim(); if (/^[0-9]/.test(s)) rollFormulaParts.push("+"); rollFormulaParts.push(s); rollModifiers.push("DM " + s); } const rollFormula = rollFormulaParts.join(""); const roll = await new Roll(rollFormula, this.actor.getRollData()).roll(); // Difficulty for Surgery is Average (8) const difficulty = 8; const effect = roll.total - difficulty; const isSuccess = effect >= 0; // Success: heal Math.max(1, effect); Failure: patient takes 3 + |effect| damage const healing = isSuccess ? Math.max(1, effect) : 0; const surgeryDamage = isSuccess ? 0 : 3 + Math.abs(effect); const cardButtons = []; if (isSuccess) { cardButtons.push({ label: game.i18n.localize("MGT2.Healing.ApplyHealing"), action: "healing" }); } else { cardButtons.push({ label: game.i18n.localize("MGT2.Healing.ApplySurgeryDamage"), action: "surgeryDamage" }); } const chatData = { user: game.user.id, speaker: ChatMessage.getSpeaker({ actor: this.actor }), formula: roll._formula, tooltip: await roll.getTooltip(), total: Math.round(roll.total * 100) / 100, rollTypeName: game.i18n.localize("MGT2.Healing.Surgery"), rollObjectName: this.actor.name, rollModifiers: rollModifiers, rollDifficulty: difficulty, rollDifficultyLabel: MGT2Helper.getDifficultyDisplay("Average"), rollEffectStr: effect.toString(), healingAmount: isSuccess ? healing : undefined, surgeryDamageAmount: isSuccess ? undefined : surgeryDamage, rollSuccess: isSuccess || undefined, rollFailure: !isSuccess || undefined, showButtons: true, hasDamage: false, showRollDamage: false, cardButtons: cardButtons, }; const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData); chatData.content = html; chatData.flags = { mgt2: { surgery: { healing, surgeryDamage } } }; return roll.toMessage(chatData); } } else if (healType === "medical") { const result = await CharacterPrompts.openHealingDays(); if (result) { const endMD = this.actor.system.characteristics.endurance.dm; const medSkill = this.actor.items.find(i => i.type === "talent" && i.system.subType === "skill" && (i.name.toLowerCase().includes("medecin") || i.name.toLowerCase().includes("medicine")) ); const skillValue = medSkill ? medSkill.system.level : 0; const days = result.days; const healingPerDay = Math.max(1, 3 + endMD + skillValue); const totalHealing = healingPerDay * days; const rollModifiers = [ `3 (base)`, `${endMD >= 0 ? "+" : ""}${endMD} END`, `+${skillValue} ${medSkill?.name ?? "Médecine"}`, `× ${days} ${game.i18n.localize("MGT2.RollPrompt.Days").toLowerCase()}` ]; const templateData = { rollObjectName: this.actor.name, rollTypeName: game.i18n.localize("MGT2.Healing.MedicalCare"), rollModifiers, formula: `${healingPerDay} ${game.i18n.localize("MGT2.Items.PerDay")}`, tooltip: "", total: totalHealing, rollSuccess: true, showButtons: true, cardButtons: [ { action: "healing", label: game.i18n.localize("MGT2.Healing.ApplyHealing") } ] }; const content = await renderTemplate( "systems/mgt2/templates/chat/roll.html", templateData ); await ChatMessage.create({ user: game.user.id, speaker: ChatMessage.getSpeaker({ actor: this.actor }), content, flags: { mgt2: { healing: { amount: totalHealing } } } }); } } else if (healType === "natural") { const result = await CharacterPrompts.openHealingDays(); if (result) { const endMD = this.actor.system.characteristics.endurance.dm; let totalAmount = 0; const rolls = []; for (let i = 0; i < result.days; i++) { const roll = await new Roll("1d6").evaluate(); const dayHealing = Math.max(1, roll.total + endMD); rolls.push({ roll, dayHealing }); totalAmount += dayHealing; } // Build roll details const rollDisplay = rolls.map((r, idx) => `