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.SchemaField({ ap: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }), auto: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }), blast: new fields$4.NumberField({ required: false, initial: 0, min: 0, integer: true }), bulky: new fields$4.BooleanField({ required: false, initial: false }), veryBulky: new fields$4.BooleanField({ required: false, initial: false }), stun: new fields$4.BooleanField({ required: false, initial: false }), smart: new fields$4.BooleanField({ required: false, initial: false }), radiation: new fields$4.BooleanField({ required: false, initial: false }), scope: new fields$4.BooleanField({ required: false, initial: false }), zeroG: new fields$4.BooleanField({ required: false, initial: false }) }); 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; } /** Returns a compact display string of active traits (e.g. "AP 2, Auto 3, Blast 5, Bulky") */ static getTraitsSummary(traits) { if (!traits) return ""; const parts = []; if (traits.ap > 0) parts.push(`AP ${traits.ap}`); if (traits.auto > 0) parts.push(`Auto ${traits.auto}`); if (traits.blast > 0) parts.push(`Blast ${traits.blast}`); if (traits.bulky) parts.push(game.i18n.localize("MGT2.WeaponTraits.Bulky")); if (traits.veryBulky) parts.push(game.i18n.localize("MGT2.WeaponTraits.VeryBulky")); if (traits.stun) parts.push(game.i18n.localize("MGT2.WeaponTraits.Stun")); if (traits.smart) parts.push(game.i18n.localize("MGT2.WeaponTraits.Smart")); if (traits.radiation) parts.push(game.i18n.localize("MGT2.WeaponTraits.Radiation")); if (traits.scope) parts.push(game.i18n.localize("MGT2.WeaponTraits.Scope")); if (traits.zeroG) parts.push(game.i18n.localize("MGT2.WeaponTraits.ZeroG")); return parts.join(", "); } } 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; // Use the threshold from updateData if it was just recalculated (e.g. STR/END talent change), // otherwise fall back to the persisted value. const encumbranceThreshold = updateData["system.inventory.encumbrance.normal"] ?? $this.system.inventory.encumbrance.normal; updateData["system.states.encumbrance"] = onHandWeight > encumbranceThreshold; 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); // Also update the encumbrance state flag against the new threshold foundry.utils.setProperty(changed, "system.states.encumbrance", $this.system.inventory.weight > normal); } //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 async applyDamage($this, amount, { ignoreArmor = false, ap = 0, stun = false } = {}) { if (isNaN(amount) || amount === 0) return { incapRounds: 0 }; const rank1 = $this.system.config.damages.rank1; const rank2 = $this.system.config.damages.rank2; const rank3 = $this.system.config.damages.rank3; if (amount < 0) amount = Math.abs(amount); if (!ignoreArmor) { const rawArmor = $this.system.inventory?.armor ?? 0; const armorValue = Math.max(0, rawArmor - ap); amount = Math.max(0, amount - armorValue); if (amount === 0) return { incapRounds: 0 }; } // ── Stun / Incapacitating: only deduct from endurance (rank3) ───────── if (stun) { const endKey = rank3; // "endurance" by default const prevEnd = $this.system.characteristics[endKey].value; const newEnd = Math.max(0, prevEnd - amount); const incapRounds = newEnd === 0 ? Math.max(0, amount - prevEnd) : 0; await $this.update({ system: { characteristics: { [endKey]: { value: newEnd, dm: this.getModifier(newEnd) } } } }); return { incapRounds }; } // ── Normal damage cascade: rank1 → rank2 → 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 }; 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; } } await $this.update({ system: { characteristics: data } }); return { incapRounds: 0 }; } 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, ap = 0, stun = false } = {}) { if (this.type === "character") { return ActorCharacter.applyDamage(this, amount, { ignoreArmor, ap, stun }); } else if (this.type === "creature") { if (isNaN(amount) || amount === 0) return Promise.resolve({ incapRounds: 0 }); if (amount < 0) amount = Math.abs(amount); const rawArmor = ignoreArmor ? 0 : (this.system.armor ?? 0); const armorValue = Math.max(0, rawArmor - ap); const effective = Math.max(0, amount - armorValue); if (effective === 0) return Promise.resolve({ incapRounds: 0 }); const newValue = Math.max(0, (this.system.life.value ?? 0) - effective); return this.update({ "system.life.value": newValue }).then(() => ({ incapRounds: 0 })); } } 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, // Ranged/Melee weapon flags isRanged: options.isRanged ?? false, isMelee: options.isMelee ?? false, isAttack: (options.isRanged ?? false) || (options.isMelee ?? false), autoLevel: options.autoLevel ?? 0, hasScope: options.scope ?? false, hasZeroG: options.zeroG ?? false, }); 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 safeTitle = title || game.i18n.localize("MGT2.Actor.Species") || "Species"; const htmlContent = await renderTemplate$2("systems/mgt2/templates/editor-fullview.html", { config: CONFIG.MGT2, html: html ?? "" }); game.settings.get("mgt2", "theme"); await DialogV2.wait({ window: { title: safeTitle }, 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); i._subInfo = WeaponData.getTraitsSummary(i.system.traits); 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, damageAP: 0, blastRadius: 0, stun: false, radiation: false, isMelee: false, isRanged: false, bulky: false, veryBulky: 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; rollOptions.isRanged = itemObj.type === "weapon" && !itemObj.system.range?.isMelee; rollOptions.damageAP = itemObj.system.traits?.ap ?? 0; rollOptions.blastRadius = itemObj.system.traits?.blast ?? 0; rollOptions.stun = itemObj.system.traits?.stun === true; rollOptions.radiation = itemObj.system.traits?.radiation === true; rollOptions.scope = itemObj.system.traits?.scope === true; rollOptions.zeroG = itemObj.system.traits?.zeroG === true; rollOptions.autoLevel = itemObj.system.traits?.auto ?? 0; rollOptions.itemId = itemObj._id; rollOptions.magazine = itemObj.system.magazine ?? -1; // -1 = not tracked rollOptions.bulky = itemObj.system.traits?.bulky === true; rollOptions.veryBulky = itemObj.system.traits?.veryBulky === 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 = []; // Auto trait — fire mode const autoLevel = rollOptions.autoLevel ?? 0; const autoMode = autoLevel > 0 ? (userRollData.autoMode ?? "single") : "single"; 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 (rollOptions.isRanged) { const rangedRange = parseInt(userRollData.rangedRange ?? "0", 10); if (rangedRange === 1) { rollFormulaParts.push("+1"); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.RangeShort") + " +1"); } else if (rangedRange === -2) { rollFormulaParts.push("-2"); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.RangeLong") + " −2"); } else if (rangedRange === -4) { rollFormulaParts.push("-4"); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.RangeExtreme") + " −4"); } const rangedAim = parseInt(userRollData.rangedAim ?? "0", 10); // Auto: burst/full-auto cancels all aiming advantages (rules p.75) if (rangedAim > 0 && autoMode === "single") { rollFormulaParts.push(`+${rangedAim}`); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Aim") + ` +${rangedAim}`); if (userRollData.rangedLaserSight === true || userRollData.rangedLaserSight === "true") { rollFormulaParts.push("+1"); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.LaserSight") + " +1"); } } const rangedFastTarget = parseInt(userRollData.rangedFastTarget ?? "0", 10); if (rangedFastTarget < 0) { rollFormulaParts.push(`${rangedFastTarget}`); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.FastTarget") + ` ${rangedFastTarget}`); } if (userRollData.rangedCover === true || userRollData.rangedCover === "true") { rollFormulaParts.push("-2"); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Cover") + " −2"); } if (userRollData.rangedProne === true || userRollData.rangedProne === "true") { rollFormulaParts.push("-1"); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Prone") + " −1"); } if (userRollData.rangedDodge === true || userRollData.rangedDodge === "true") { const dodgeDM = parseInt(userRollData.rangedDodgeDM ?? "0", 10); if (dodgeDM < 0) { rollFormulaParts.push(`${dodgeDM}`); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Dodge") + ` ${dodgeDM}`); } } } if (rollOptions.isMelee) { if (userRollData.meleeDodge === true || userRollData.meleeDodge === "true") { const dodgeDM = parseInt(userRollData.meleeDodgeDM ?? "0", 10); if (dodgeDM < 0) { rollFormulaParts.push(`${dodgeDM}`); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Dodge") + ` ${dodgeDM}`); } } if (userRollData.meleeParry === true || userRollData.meleeParry === "true") { const parryDM = parseInt(userRollData.meleeParryDM ?? "0", 10); if (parryDM < 0) { rollFormulaParts.push(`${parryDM}`); rollModifiers.push(game.i18n.localize("MGT2.RollPrompt.Parry") + ` ${parryDM}`); } } } if (MGT2Helper.hasValue(userRollData, "difficulty") && userRollData.difficulty !== "") rollOptions.difficulty = userRollData.difficulty; // ── Bulky / Very Bulky trait: STR penalty ──────────────────────────── const strDm = this.actor.system.characteristics.strength?.dm ?? 0; if (rollOptions.veryBulky) { // Very Bulky: requires STR DM ≥ +2 if (strDm < 2) { const penalty = strDm - 2; rollFormulaParts.push(`${penalty}`); rollModifiers.push(game.i18n.localize("MGT2.WeaponTraits.VeryBulky") + ` ${penalty}`); ui.notifications.warn(game.i18n.format("MGT2.Notifications.VeryBulkyPenalty", { penalty })); } } else if (rollOptions.bulky) { // Bulky: requires STR DM ≥ +1 if (strDm < 1) { const penalty = strDm - 1; rollFormulaParts.push(`${penalty}`); rollModifiers.push(game.i18n.localize("MGT2.WeaponTraits.Bulky") + ` ${penalty}`); ui.notifications.warn(game.i18n.format("MGT2.Notifications.BulkyPenalty", { penalty })); } } const rollFormula = rollFormulaParts.join(""); if (!Roll.validate(rollFormula)) { ui.notifications.error(game.i18n.localize("MGT2.Errors.InvalidRollFormula")); return; } // ── Ammo decrement (ranged weapons with magazine tracking) ─────────── if (rollOptions.isRanged && rollOptions.itemId && rollOptions.magazine >= 0) { let ammoUsed = 1; if (autoMode === "burst" && autoLevel > 0) ammoUsed = autoLevel; else if (autoMode === "fullAuto" && autoLevel > 0) ammoUsed = autoLevel * 3; const currentMag = rollOptions.magazine; if (currentMag <= 0) { ui.notifications.warn( game.i18n.format("MGT2.Notifications.NoAmmo", { weapon: rollOptions.rollObjectName }) ); } else { const newMag = Math.max(0, currentMag - ammoUsed); const weaponItem = this.actor.getEmbeddedDocument("Item", rollOptions.itemId); if (weaponItem) await weaponItem.update({ "system.magazine": newMag }); if (newMag === 0) { ui.notifications.warn( game.i18n.format("MGT2.Notifications.AmmoEmpty", { weapon: rollOptions.rollObjectName }) ); } else { ui.notifications.info( game.i18n.format("MGT2.Notifications.AmmoUsed", { used: ammoUsed, remaining: newMag, weapon: rollOptions.rollObjectName }) ); } } } 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) + Auto burst 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; } // Burst: add Auto level to damage if (autoMode === "burst" && autoLevel > 0) { effectiveDamageFormula += `+${autoLevel}`; } } // Auto fire mode chat info let autoInfo = null; if (autoMode === "burst" && autoLevel > 0) { autoInfo = game.i18n.format("MGT2.RollPrompt.AutoBurstInfo", { level: autoLevel, ammo: autoLevel }); } else if (autoMode === "fullAuto" && autoLevel > 0) { autoInfo = game.i18n.format("MGT2.RollPrompt.AutoFullInfo", { level: autoLevel, ammo: autoLevel * 3 }); } // ── 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, autoInfo, }; 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, ap: rollOptions.damageAP ?? 0, blast: rollOptions.blastRadius ?? 0, stun: rollOptions.stun ?? false, radiation: rollOptions.radiation ?? false, 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(); const title = this.actor.system.personal.species || game.i18n.localize("MGT2.Actor.Species") || "Species"; const html = this.actor.system.personal.speciesText?.descriptionLong ?? ""; await CharacterPrompts.openEditorFullView(title, html); } static async #onHeal(event, target) { event.preventDefault(); const healType = target.dataset.healType; if (game.user.targets.size === 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) => `
${game.i18n.localize("MGT2.RollPrompt.Days")} ${idx + 1}: 1d6 = ${r.roll.total} + ${endMD > 0 ? "+" : ""}${endMD} = ${r.dayHealing}
` ).join(""); const chatData = { user: game.user.id, speaker: ChatMessage.getSpeaker({ actor: this.actor }), content: `
${this.actor.name}
${game.i18n.localize("MGT2.Healing.NaturalHealing")}
${result.days} ${game.i18n.localize("MGT2.RollPrompt.Days")}
${rollDisplay}
${game.i18n.localize("MGT2.Chat.Roll.Effect")} ${totalAmount}
`, flags: { mgt2: { healing: { amount: totalAmount } } } }; await ChatMessage.create(chatData); // Apply healing immediately await this.actor.applyHealing(totalAmount); ui.notifications.info( game.i18n.format("MGT2.Notifications.HealingApplied", { name: this.actor.name, amount: totalAmount }) ); } } } } class TravellerVehiculeSheet extends MGT2ActorSheet { /** @override */ static DEFAULT_OPTIONS = { ...super.DEFAULT_OPTIONS, classes: [...super.DEFAULT_OPTIONS.classes, "vehicule", "nopad"], window: { ...super.DEFAULT_OPTIONS.window, title: "TYPES.Actor.vehicule", }, } /** @override */ static PARTS = { sheet: { template: "systems/mgt2/templates/actors/vehicule-sheet.html", }, } /** @override */ tabGroups = { primary: "stats" } /** @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.enrichedDescription = await enrich(actor.system.description); context.enrichedNotes = await enrich(actor.system.notes); return context; } } const { renderTemplate: renderTemplate$1 } = foundry.applications.handlebars; /** Convert Traveller dice notation (e.g. "2D", "4D+2", "3D6") to FoundryVTT formula */ function normalizeDice(formula) { if (!formula) return "1d6"; return formula .replace(/(\d*)D(\d*)([+-]\d+)?/gi, (_, count, sides, mod) => { const n = count || "1"; const d = sides || "6"; return mod ? `${n}d${d}${mod}` : `${n}d${d}`; }); } class TravellerCreatureSheet extends MGT2ActorSheet { /** @override */ static DEFAULT_OPTIONS = { ...super.DEFAULT_OPTIONS, classes: [...super.DEFAULT_OPTIONS.classes, "creature", "nopad"], position: { width: 720, height: 600, }, window: { ...super.DEFAULT_OPTIONS.window, title: "TYPES.Actor.creature", }, actions: { ...super.DEFAULT_OPTIONS.actions, rollAttack: TravellerCreatureSheet.#onRollAttack, rollSkill: TravellerCreatureSheet.#onRollSkill, addSkill: TravellerCreatureSheet.#onAddRow, deleteSkill: TravellerCreatureSheet.#onDeleteRow, addAttack: TravellerCreatureSheet.#onAddRow, deleteAttack: TravellerCreatureSheet.#onDeleteRow, addTrait: TravellerCreatureSheet.#onAddRow, deleteTrait: TravellerCreatureSheet.#onDeleteRow, }, } /** @override */ static PARTS = { sheet: { template: "systems/mgt2/templates/actors/creature-sheet.html", }, } /** @override */ tabGroups = { primary: "combat" } /** @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.sizeLabel = this._getSizeLabel(actor.system.life.max); context.sizeTraitLabel = this._getSizeTrait(actor.system.life.max); context.config = CONFIG.MGT2; return context; } _getSizeLabel(pdv) { if (pdv <= 2) return "Souris / Rat"; if (pdv <= 5) return "Chat"; if (pdv <= 7) return "Blaireau / Chien"; if (pdv <= 13) return "Chimpanzé / Chèvre"; if (pdv <= 28) return "Humain"; if (pdv <= 35) return "Vache / Cheval"; if (pdv <= 49) return "Requin"; if (pdv <= 70) return "Rhinocéros"; if (pdv <= 90) return "Éléphant"; if (pdv <= 125) return "Carnosaure"; return "Sauropode / Baleine"; } _getSizeTrait(pdv) { if (pdv <= 2) return "Petit (−4)"; if (pdv <= 5) return "Petit (−3)"; if (pdv <= 7) return "Petit (−2)"; if (pdv <= 13) return "Petit (−1)"; if (pdv <= 28) return "—"; if (pdv <= 35) return "Grand (+1)"; if (pdv <= 49) return "Grand (+2)"; if (pdv <= 70) return "Grand (+3)"; if (pdv <= 90) return "Grand (+4)"; if (pdv <= 125) return "Grand (+5)"; return "Grand (+6)"; } // ───────────────────────────────────────────────────────── Roll Helpers static async #postCreatureRoll({ actor, roll, rollLabel, dm, difficulty, difficultyLabel, rollMode, extraTooltip, damageFormula }) { const diffTarget = MGT2Helper.getDifficultyValue(difficulty ?? "Average"); const hasDifficulty = !!difficulty; const success = hasDifficulty ? roll.total >= diffTarget : true; const effect = roll.total - diffTarget; const effectStr = (effect >= 0 ? "+" : "") + effect; const diceRawTotal = roll.dice.reduce((s, d) => s + d.total, 0); const breakdownParts = [game.i18n.localize("MGT2.Chat.Roll.Dice") + " " + diceRawTotal]; if (dm !== 0) breakdownParts.push(`DM ${dm >= 0 ? "+" : ""}${dm}`); if (hasDifficulty) breakdownParts.push(game.i18n.localize("MGT2.Chat.Roll.Effect") + " " + effectStr); if (extraTooltip) breakdownParts.push(extraTooltip); const rollBreakdown = breakdownParts.join(" | "); const showRollDamage = success && !!damageFormula; const chatData = { creatureName: actor.name, creatureImg: actor.img, rollLabel, formula: roll.formula, total: roll.total, tooltip: await roll.getTooltip(), rollBreakdown, difficulty: hasDifficulty ? diffTarget : null, difficultyLabel: difficultyLabel ?? MGT2Helper.getDifficultyDisplay(difficulty), success: hasDifficulty ? success : null, failure: hasDifficulty ? !success : null, effect: hasDifficulty ? effect : null, effectStr: hasDifficulty ? effectStr : null, modifiers: dm !== 0 ? [`DM ${dm >= 0 ? "+" : ""}${dm}`] : [], showRollDamage, }; const chatContent = await renderTemplate$1( "systems/mgt2/templates/chat/creature-roll.html", chatData ); const flags = showRollDamage ? { mgt2: { damage: { formula: normalizeDice(damageFormula), effect, rollObjectName: actor.name, rollTypeName: rollLabel, } } } : {}; await ChatMessage.create({ content: chatContent, speaker: ChatMessage.getSpeaker({ actor }), rolls: [roll], rollMode: rollMode ?? game.settings.get("core", "rollMode"), flags, }); return { success, effect, total: roll.total }; } // ───────────────────────────────────────────────────────── Roll Handlers /** Roll a skill check (2d6 + level vs difficulty) — uses unified dialog */ static async #onRollSkill(event, target) { const index = parseInt(target.dataset.index ?? 0); const actor = this.document; const skill = actor.system.skills[index]; if (!skill) return; const result = await RollPromptHelper.roll({ isCreature: true, showSkillSelector: false, skillName: skill.name, skillLevel: skill.level, difficulty: "Average", title: game.i18n.localize("MGT2.Creature.RollSkill") + " — " + skill.name, }); if (!result) return; const customDM = parseInt(result.customDM ?? "0", 10) || 0; const skillLevel = parseInt(skill.level ?? 0, 10) || 0; const dm = skillLevel + customDM; const diceModifier = result.diceModifier ?? ""; // Build formula exactly like character-sheet: parts joined without spaces const parts = []; if (diceModifier) { parts.push("3d6", diceModifier); } else { parts.push("2d6"); } if (dm !== 0) parts.push(MGT2Helper.getFormulaDM(dm)); const fullFormula = parts.join(""); const roll = await new Roll(fullFormula).evaluate(); const rollLabel = `${skill.name.toUpperCase()} (${skillLevel >= 0 ? "+" : ""}${skillLevel})`; const tooltipParts = [`Dés: ${roll.dice.reduce((s, d) => s + d.total, 0)}`]; if (skillLevel !== 0) tooltipParts.push(`${skill.name} ${skillLevel >= 0 ? "+" : ""}${skillLevel}`); if (customDM !== 0) tooltipParts.push(`MD perso ${customDM >= 0 ? "+" : ""}${customDM}`); await TravellerCreatureSheet.#postCreatureRoll({ actor, roll, rollLabel, dm, difficulty: result.difficulty, rollMode: result.rollMode, extraTooltip: tooltipParts.join(" | "), }); } /** Roll an attack: dialog with skill selector, then roll 2d6+skill+DM vs difficulty; on success roll damage */ static async #onRollAttack(event, target) { const index = parseInt(target.dataset.index ?? 0); const actor = this.document; const attack = actor.system.attacks[index]; if (!attack) return; const skills = actor.system.skills ?? []; const result = await RollPromptHelper.roll({ isCreature: true, showSkillSelector: true, creatureSkills: skills, selectedSkillIndex: attack.skill ?? -1, difficulty: "Average", title: game.i18n.localize("MGT2.Creature.RollAttack") + " — " + attack.name, }); if (!result) return; const skillIndex = parseInt(result.creatureSkillIndex ?? "-1", 10); const chosenSkill = (skillIndex >= 0 && skillIndex < skills.length) ? skills[skillIndex] : null; const skillLevel = parseInt(chosenSkill?.level ?? 0, 10) || 0; const customDM = parseInt(result.customDM ?? "0", 10) || 0; const dm = skillLevel + customDM; const diceModifier = result.diceModifier ?? ""; // Build formula exactly like character-sheet: parts joined without spaces const parts = []; if (diceModifier) { parts.push("3d6", diceModifier); } else { parts.push("2d6"); } if (dm !== 0) parts.push(MGT2Helper.getFormulaDM(dm)); const fullFormula = parts.join(""); const roll = await new Roll(fullFormula).evaluate(); const rollLabel = chosenSkill ? `${attack.name} — ${chosenSkill.name} (${skillLevel >= 0 ? "+" : ""}${skillLevel})` : attack.name; const tooltipParts = [`Dés: ${roll.dice.reduce((s, d) => s + d.total, 0)}`]; if (chosenSkill) tooltipParts.push(`${chosenSkill.name} ${skillLevel >= 0 ? "+" : ""}${skillLevel}`); if (customDM !== 0) tooltipParts.push(`MD perso ${customDM >= 0 ? "+" : ""}${customDM}`); await TravellerCreatureSheet.#postCreatureRoll({ actor, roll, rollLabel, dm, difficulty: result.difficulty, rollMode: result.rollMode, extraTooltip: tooltipParts.join(" | "), damageFormula: attack.damage || null, }); } // ───────────────────────────────────────────────────────── CRUD Handlers static async #onAddRow(event, target) { const prop = target.dataset.prop; if (!prop) return; const actor = this.document; const arr = foundry.utils.deepClone(actor.system[prop] ?? []); arr.push(this._getDefaultRow(prop)); await actor.update({ [`system.${prop}`]: arr }); } static async #onDeleteRow(event, target) { const prop = target.dataset.prop; const index = parseInt(target.dataset.index); if (!prop || isNaN(index)) return; const actor = this.document; const arr = foundry.utils.deepClone(actor.system[prop] ?? []); arr.splice(index, 1); await actor.update({ [`system.${prop}`]: arr }); } _getDefaultRow(prop) { switch (prop) { case "skills": return { name: "", level: 0, note: "" }; case "attacks": return { name: "", damage: "1D", skill: -1, description: "" }; case "traits": return { name: "", value: "", description: "" }; default: return {}; } } } const { HandlebarsApplicationMixin } = foundry.applications.api; class TravellerItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) { /** @override */ static DEFAULT_OPTIONS = { classes: ["mgt2", "sheet", "item"], position: { width: 630 }, form: { submitOnChange: true, closeOnSubmit: false, }, window: { resizable: true }, actions: { careerEventCreate: TravellerItemSheet.#onCareerEventCreate, careerEventDelete: TravellerItemSheet.#onCareerEventDelete, optionCreate: TravellerItemSheet.#onOptionCreate, optionDelete: TravellerItemSheet.#onOptionDelete, modifierCreate: TravellerItemSheet.#onModifierCreate, modifierDelete: TravellerItemSheet.#onModifierDelete, }, } /** Dynamic PARTS: template resolved per item type */ get PARTS() { const type = this.document?.type ?? "item"; return { sheet: { template: `systems/mgt2/templates/items/${type}-sheet.html`, }, }; } /** Resolve template dynamically based on item type */ get template() { return `systems/mgt2/templates/items/${this.document.type}-sheet.html`; } tabGroups = { primary: "tab1" } /** @override */ async _prepareContext() { const item = this.document; const source = item.toObject(); const settings = { usePronouns: game.settings.get("mgt2", "usePronouns"), }; let containers = null; let computers = null; let hadContainer = false; if (item.actor !== null) { hadContainer = true; containers = [{ name: "", _id: "" }].concat(item.actor.getContainers()); computers = [{ name: "", _id: "" }].concat(item.actor.getComputers()); } let weight = null; if (item.system.hasOwnProperty("weight")) { weight = MGT2Helper.convertWeightForDisplay(item.system.weight); } let skills = []; if (this.actor !== null) { for (let actorItem of this.actor.items) { if (actorItem.type === "talent" && actorItem.system.subType === "skill") skills.push({ _id: actorItem._id, name: actorItem.getRollDisplay() }); } } skills.sort(MGT2Helper.compareByName); skills = [{ _id: "NP", name: game.i18n.localize("MGT2.Items.NotProficient") }].concat(skills); const enrich = (html) => foundry.applications.ux.TextEditor.implementation.enrichHTML(html ?? "", { async: true }); return { item: item, document: item, cssClass: this.isEditable ? "editable" : "locked", system: item.system, source: source.system, fields: item.schema.fields, systemFields: item.system.schema.fields, isEditable: this.isEditable, isGM: game.user.isGM, config: CONFIG.MGT2, settings: settings, containers: containers, computers: computers, hadContainer: hadContainer, weight: weight, unitlabels: { weight: MGT2Helper.getWeightLabel() }, skills: skills, enrichedDescription: await enrich(item.system.description), enrichedDescriptionLong: await enrich(item.system.descriptionLong), enrichedNotes: await enrich(item.system.notes), enrichedLockedDescription: await enrich(item.system.lockedDescription), }; } /** @override — resolve the per-type template before rendering */ async _renderHTML(context, options) { const templatePath = `systems/mgt2/templates/items/${this.document.type}-sheet.html`; const html = await foundry.applications.handlebars.renderTemplate(templatePath, context); return { sheet: html }; } /** @override — put rendered HTML into the window content */ _replaceHTML(result, content, options) { content.innerHTML = result.sheet; // 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(); this._bindItemEvents(); } /** Bind CSS class-based events (templates not yet migrated to data-action) */ _bindItemEvents() { const html = this.element; if (!this.isEditable) return; const bind = (sel, handler) => { for (const el of html.querySelectorAll(sel)) { el.addEventListener("click", (ev) => handler.call(this, ev, ev.currentTarget)); } }; bind(".event-create", TravellerItemSheet.#onCareerEventCreate); bind(".event-delete", TravellerItemSheet.#onCareerEventDelete); bind(".options-create", TravellerItemSheet.#onOptionCreate); bind(".options-delete", TravellerItemSheet.#onOptionDelete); bind(".modifiers-create", TravellerItemSheet.#onModifierCreate); bind(".modifiers-delete", TravellerItemSheet.#onModifierDelete); // Activate ProseMirror editors for HTMLField fields for (const btn of html.querySelectorAll(".editor-edit")) { btn.addEventListener("click", async (event) => { event.preventDefault(); const editorWrapper = btn.closest(".editor"); if (!editorWrapper) return; const editorContent = editorWrapper.querySelector(".editor-content"); if (!editorContent || editorContent.classList.contains("ProseMirror")) return; const target = editorContent.dataset.edit; const value = foundry.utils.getProperty(this.document, target) ?? ""; btn.remove(); editorWrapper.classList.add("prosemirror"); await ProseMirrorEditor.create(editorContent, value, { document: this.document, fieldName: target, plugins: {}, collaborate: false, }); }); } } _activateTabGroups() { for (const [group, activeTab] of Object.entries(this.tabGroups)) { const nav = this.element.querySelector(`nav[data-group="${group}"], .horizontal-tabs`); 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(`.itemsheet-panel [data-tab], [data-group="${group}"][data-tab]`).forEach(content => { content.classList.toggle('active', content.dataset.tab === activeTab); }); } } /** @override — process form data before submit (weight/qty/cost conversions + container logic) */ _prepareSubmitData(event, form, formData) { const data = foundry.utils.expandObject(formData.object); if (data.hasOwnProperty("weight")) { data.system = data.system || {}; data.system.weight = MGT2Helper.convertWeightFromInput(data.weight); delete data.weight; } if (data.system?.hasOwnProperty("quantity")) { data.system.quantity = MGT2Helper.getIntegerFromInput(data.system.quantity); } if (data.system?.hasOwnProperty("cost")) { data.system.cost = MGT2Helper.getIntegerFromInput(data.system.cost); } // Container/equipped logic if (data.system?.hasOwnProperty("container") && this.document.system.hasOwnProperty("equipped")) { const equippedChange = this.document.system.equipped !== data.system.equipped; const containerChange = this.document.system.container?.id !== data.system.container?.id; if (equippedChange && data.system.equipped === true) { data.system.container = { id: "" }; } else if (containerChange && data.system.container?.id !== "" && this.document.system.container?.id === "") { data.system.equipped = false; } } return foundry.utils.flattenObject(data); } // ========================================================= // Actions // ========================================================= static async #onCareerEventCreate(event) { event.preventDefault(); const events = this.document.system.events; let newEvents; if (!events || events.length === 0) { newEvents = [{ age: "", description: "" }]; } else { newEvents = [...events, { age: "", description: "" }]; } return this.document.update({ system: { events: newEvents } }); } static async #onCareerEventDelete(event, target) { event.preventDefault(); const element = target.closest("[data-events-part]"); const index = Number(element.dataset.eventsPart); const events = foundry.utils.deepClone(this.document.system.events); const newEvents = Object.entries(events) .filter(([key]) => Number(key) !== index) .map(([, val]) => val); return this.document.update({ system: { events: newEvents } }); } static async #onOptionCreate(event, target) { event.preventDefault(); const property = target.dataset.property; const options = this.document.system[property]; let newOptions; if (!options || options.length === 0) { newOptions = [{ name: "", description: "" }]; } else { newOptions = [...options, { name: "", description: "" }]; } return this.document.update({ [`system.${property}`]: newOptions }); } static async #onOptionDelete(event, target) { event.preventDefault(); const element = target.closest("[data-options-part]"); const property = element.dataset.property; const index = Number(element.dataset.optionsPart); const options = foundry.utils.deepClone(this.document.system[property]); const newOptions = Object.entries(options) .filter(([key]) => Number(key) !== index) .map(([, val]) => val); return this.document.update({ [`system.${property}`]: newOptions }); } static async #onModifierCreate(event) { event.preventDefault(); const modifiers = this.document.system.modifiers; let newModifiers; if (!modifiers || modifiers.length === 0) { newModifiers = [{ characteristic: "Endurance", value: null }]; } else { newModifiers = [...modifiers, { characteristic: "Endurance", value: null }]; } return this.document.update({ system: { modifiers: newModifiers } }); } static async #onModifierDelete(event, target) { event.preventDefault(); const element = target.closest("[data-modifiers-part]"); const index = Number(element.dataset.modifiersPart); const modifiers = foundry.utils.deepClone(this.document.system.modifiers); const newModifiers = Object.entries(modifiers) .filter(([key]) => Number(key) !== index) .map(([, val]) => val); return this.document.update({ system: { modifiers: newModifiers } }); } } /** * Define a set of template paths to pre-load * Pre-loaded templates are compiled and cached for fast access when rendering * @return {Promise} */ const preloadHandlebarsTemplates = async function() { const templatePaths = [ "systems/mgt2/templates/items/armor-sheet.html", "systems/mgt2/templates/items/career-sheet.html", "systems/mgt2/templates/items/computer-sheet.html", "systems/mgt2/templates/items/contact-sheet.html", "systems/mgt2/templates/items/container-sheet.html", "systems/mgt2/templates/items/disease-sheet.html", "systems/mgt2/templates/items/equipment-sheet.html", "systems/mgt2/templates/items/item-sheet.html", "systems/mgt2/templates/items/species-sheet.html", "systems/mgt2/templates/items/talent-sheet.html", "systems/mgt2/templates/items/weapon-sheet.html", "systems/mgt2/templates/items/parts/sheet-configuration.html", "systems/mgt2/templates/items/parts/sheet-physical-item.html", "systems/mgt2/templates/items/parts/sheet-physical-item-tab.html", "systems/mgt2/templates/roll-prompt.html", "systems/mgt2/templates/chat/roll.html", //"systems/mgt2/templates/chat/roll-characteristic.html", "systems/mgt2/templates/chat/radiation.html", "systems/mgt2/templates/actors/actor-config-sheet.html", "systems/mgt2/templates/actors/actor-config-characteristic-sheet.html", "systems/mgt2/templates/actors/trait-sheet.html", "systems/mgt2/templates/actors/creature-sheet.html", "systems/mgt2/templates/chat/creature-roll.html", "systems/mgt2/templates/editor-fullview.html" ]; const loader = foundry.applications?.handlebars?.loadTemplates ?? loadTemplates; return loader(templatePaths); }; class ChatHelper { static setupCardListeners(message, element, messageData) { if (!message || !element) { return; } // Restore disabled state for already-applied buttons const appliedActions = message.flags?.mgt2?.appliedActions ?? []; if (appliedActions.length > 0) { appliedActions.forEach(action => { element.querySelectorAll(`button[data-action="${action}"]`).forEach(btn => { btn.disabled = true; }); }); } // Apply buttons are GM-only: hide them for players const GM_ACTIONS = ["damage", "healing", "surgeryDamage"]; if (!game.user.isGM) { GM_ACTIONS.forEach(action => { element.querySelectorAll(`button[data-action="${action}"]`).forEach(btn => { btn.style.display = "none"; }); }); // Hide the buttons container if no visible buttons remain element.querySelectorAll(".mgt2-buttons").forEach(container => { const hasVisible = [...container.querySelectorAll("button")] .some(b => b.style.display !== "none"); if (!hasVisible) container.style.display = "none"; }); } element.querySelectorAll('button[data-action="rollDamage"]').forEach(el => { el.addEventListener('click', async event => { await this._processRollDamageButtonEvent(message, event); }); }); element.querySelectorAll('button[data-action="rollRadiation"]').forEach(el => { el.addEventListener('click', async event => { await this._rollRadiationDamage(message, event); }); }); element.querySelectorAll('button[data-action="damage"]').forEach(el => { el.addEventListener('click', async event => { await this._applyChatCardDamage(message, event); }); }); element.querySelectorAll('button[data-action="healing"]').forEach(el => { el.addEventListener('click', async event => { await this._applyChatCardHealing(message, event); }); }); element.querySelectorAll('button[data-action="surgeryDamage"]').forEach(el => { el.addEventListener('click', async event => { await this._applyChatCardSurgeryDamage(message, event); }); }); element.querySelectorAll('button[data-index]:not([data-action])').forEach(el => { el.addEventListener('click', async event => { await this._processRollButtonEvent(message, event); }); }); } static async _processRollButtonEvent(message, event) { event.preventDefault(); event.stopPropagation(); let buttons = message.flags.mgt2.buttons; const index = event.target.dataset.index; const button = buttons[index]; let roll = await new Roll(button.formula, {}).roll(); const chatData = { user: game.user.id, speaker: message.speaker, formula: roll._formula, tooltip: await roll.getTooltip(), total: Math.round(roll.total * 100) / 100, rollObjectName: button.message.objectName, rollMessage: MGT2Helper.format(button.message.flavor, Math.round(roll.total * 100) / 100), }; const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData); chatData.content = html; return roll.toMessage(chatData); } static async _processRollDamageButtonEvent(message, event) { event.preventDefault(); event.stopPropagation(); const damageFlags = message.flags?.mgt2?.damage; if (!damageFlags?.formula) { ui.notifications.warn(game.i18n.localize("MGT2.Errors.InvalidRollFormula")); return; } const effect = damageFlags.effect ?? 0; const effectPart = effect > 0 ? `+${effect}` : effect < 0 ? `${effect}` : ""; const rollFormula = damageFlags.formula + effectPart; let roll; try { roll = await new Roll(rollFormula, {}).roll(); } catch (e) { ui.notifications.warn(game.i18n.localize("MGT2.Errors.InvalidRollFormula")); return; } const rollTypeName = damageFlags.rollTypeName ? damageFlags.rollTypeName + " " + game.i18n.localize("MGT2.Actor.Damage") : null; const ap = damageFlags.ap ?? 0; const blast = damageFlags.blast ?? 0; const stun = damageFlags.stun ?? false; const radiation = damageFlags.radiation ?? false; const chatData = { user: game.user.id, speaker: message.speaker, formula: roll._formula, tooltip: await roll.getTooltip(), total: Math.round(roll.total * 100) / 100, showButtons: true, hasDamage: true, rollTypeName, rollObjectName: damageFlags.rollObjectName, apValue: ap > 0 ? ap : null, blastRadius: blast > 0 ? blast : null, stunWeapon: stun || null, radiationWeapon: radiation || null, }; const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData); chatData.content = html; // Persist ap, blast, stun, radiation in damage message flags so handlers can read them if (ap > 0 || blast > 0 || stun || radiation) chatData.flags = { mgt2: { damage: { ap, blast, stun, radiation, rollObjectName: damageFlags.rollObjectName } } }; return roll.toMessage(chatData); } static async #markButtonApplied(message, btn, action) { const existing = message.flags?.mgt2?.appliedActions ?? []; if (!existing.includes(action)) { await message.setFlag("mgt2", "appliedActions", [...existing, action]); } if (btn) btn.disabled = true; } static async _applyChatCardDamage(message, event) { if (game.user.targets.size === 0) { ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected")); return; } const btn = event.currentTarget; const roll = message.rolls[0]; const amount = Math.round(roll.total * 100) / 100; const ap = message.flags?.mgt2?.damage?.ap ?? 0; const stun = message.flags?.mgt2?.damage?.stun ?? false; await Promise.all([...game.user.targets].map(async t => { const result = await t.actor.applyDamage(amount, { ap, stun }); if (stun) { const incapRounds = result?.incapRounds ?? 0; if (incapRounds > 0) { ui.notifications.warn(game.i18n.format("MGT2.Notifications.StunIncapacitated", { name: t.actor.name, rounds: incapRounds })); } else { ui.notifications.info(game.i18n.format("MGT2.Notifications.StunDamageApplied", { name: t.actor.name, amount })); } } else { const notifKey = ap > 0 ? "MGT2.Notifications.DamageAppliedAP" : "MGT2.Notifications.DamageApplied"; ui.notifications.info(game.i18n.format(notifKey, { name: t.actor.name, amount, ap })); } })); await ChatHelper.#markButtonApplied(message, btn, "damage"); } static async _applyChatCardHealing(message, event) { if (game.user.targets.size === 0) { ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected")); return; } const btn = event.currentTarget; const amount = message.flags?.mgt2?.healing?.amount ?? message.flags?.mgt2?.surgery?.healing ?? Math.max(1, message.rolls[0].total); await Promise.all([...game.user.targets].map(async t => { await t.actor.applyHealing(amount); ui.notifications.info(game.i18n.format("MGT2.Notifications.HealingApplied", { name: t.actor.name, amount })); })); await ChatHelper.#markButtonApplied(message, btn, "healing"); } static async _applyChatCardSurgeryDamage(message, event) { if (game.user.targets.size === 0) { ui.notifications.warn(game.i18n.localize("MGT2.Errors.NoTokenSelected")); return; } const btn = event.currentTarget; const amount = message.flags?.mgt2?.surgery?.surgeryDamage ?? 3; await Promise.all([...game.user.targets].map(async t => { await t.actor.applyDamage(amount, { ignoreArmor: true }); ui.notifications.info(game.i18n.format("MGT2.Notifications.DamageApplied", { name: t.actor.name, amount })); })); await ChatHelper.#markButtonApplied(message, btn, "surgeryDamage"); } static async _rollRadiationDamage(message, event) { event.preventDefault(); event.stopPropagation(); const damageFlags = message.flags?.mgt2?.damage; const rollObjectName = damageFlags?.rollObjectName ?? ""; // 2D × 20 rads (MGT2 rule: 2d6 × 20) const roll = await new Roll("2d6 * 20", {}).roll(); const chatData = { user: game.user.id, speaker: message.speaker, formula: roll._formula, tooltip: await roll.getTooltip(), total: Math.round(roll.total), rollObjectName, }; const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/radiation.html", chatData); chatData.content = html; return roll.toMessage(chatData); } } const registerSettings = function () { game.settings.register("mgt2", "theme", { name: "MGT2.Settings.theme.name", hint: "MGT2.Settings.theme.hint", scope: "client", config: true, default: "black-and-red", type: String, choices: { "black-and-red": "MGT2.Themes.BlackAndRed", "mwamba": "MGT2.Themes.Mwamba", "blue": "MGT2.Themes.Blue" }, requiresReload: true }); game.settings.register('mgt2', 'usePronouns', { name: "MGT2.Settings.usePronouns.name", hint: "MGT2.Settings.usePronouns.hint", default: false, scope: 'world', type: Boolean, config: true, requiresReload: false }); game.settings.register('mgt2', 'useGender', { name: "MGT2.Settings.useGender.name", hint: "MGT2.Settings.useGender.hint", default: false, scope: 'world', type: Boolean, config: true, requiresReload: false }); game.settings.register('mgt2', 'showLife', { name: "MGT2.Settings.showLife.name", hint: "MGT2.Settings.showLife.hint", default: false, scope: 'world', type: Boolean, config: true, requiresReload: false }); // game.settings.register('mgt2', 'useWeightMetric', { // name: "MGT2.Settings.useWeightMetric.name", // hint: "MGT2.Settings.useWeightMetric.hint", // default: true, // scope: 'world', // type: Boolean, // config: true, // requiresReload: true // }); // game.settings.register('mgt2', 'useDistanceMetric', { // name: "MGT2.Settings.useDistanceMetric.name", // hint: "MGT2.Settings.useDistanceMetric.hint", // default: true, // scope: 'world', // type: Boolean, // config: true, // requiresReload: true // }); // game.settings.register('mgt2', 'showTrash', { // name: "Show Trash tab to Player", // hint: "Player can see the Trash tab and recover item", // default: false, // scope: 'world', // type: Boolean, // config: true, // requiresReload: false // }); /*game.settings.register('mgt2', 'containerDropIn', { name: "Test", hint: "Mon hint", default: true, scope: 'client', type: Boolean, config: true });*/ }; function registerHandlebarsHelpers() { Handlebars.registerHelper('showDM', function (dm) { if (dm === 0) return "0"; if (dm > 0) return `+${dm}`; if (dm < 0) return `${dm}`; return ""; }); } Hooks.once("init", async function () { CONFIG.MGT2 = MGT2; CONFIG.Combat.initiative = { formula: "2d6 + @initiative", decimals: 2 }; CONFIG.Actor.trackableAttributes = { character: { bar: ["life", "characteristics.strength", "characteristics.dexterity", "characteristics.endurance", "characteristics.intellect", "characteristics.education", "characteristics.social", "characteristics.morale", "characteristics.luck", "characteristics.sanity", "characteristics.charm", "characteristics.psionic", "characteristics.other" ], value: ["life.value", "health.radiations", "characteristics.strength.value", "characteristics.dexterity.value", "characteristics.endurance.value", "characteristics.intellect.value", "characteristics.education.value", "characteristics.social.value", "characteristics.morale.value", "characteristics.luck.value", "characteristics.sanity.value", "characteristics.charm.value", "characteristics.psionic.value", "characteristics.other.value"] }, creature: { bar: ["life"], value: ["life.value", "life.max", "speed", "armor", "psi"] } }; game.mgt2 = { TravellerActor, TravellerItem }; registerHandlebarsHelpers(); registerSettings(); CONFIG.Combatant.documentClass = MGT2Combatant; CONFIG.Actor.documentClass = TravellerActor; CONFIG.Item.documentClass = TravellerItem; foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet); foundry.documents.collections.Actors.registerSheet("mgt2", TravellerCharacterSheet, { types: ["character"], makeDefault: true, label: "Traveller Sheet" }); foundry.documents.collections.Actors.registerSheet("mgt2", TravellerVehiculeSheet, { types: ["vehicule"], makeDefault: true, label: "Vehicule Sheet" }); foundry.documents.collections.Actors.registerSheet("mgt2", TravellerCreatureSheet, { types: ["creature"], makeDefault: true, label: "Creature Sheet" }); foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet); foundry.documents.collections.Items.registerSheet("mgt2", TravellerItemSheet, { makeDefault: true }); Object.assign(CONFIG.Actor.dataModels, { "character": CharacterData, "vehicule": VehiculeData, "creature": CreatureData }); Object.assign(CONFIG.Item.dataModels, { "item": ItemData, "equipment": EquipmentData, "disease": DiseaseData, "career": CareerData, "talent": TalentData, "contact": ContactData, "weapon": WeaponData, "computer": ComputerData, "armor": ArmorData, "container": ItemContainerData, "species": SpeciesData }); Hooks.on("renderChatMessageHTML", (message, element, messageData) => { ChatHelper.setupCardListeners(message, element, messageData); }); // Preload template partials await preloadHandlebarsTemplates(); }); export { MGT2 }; //# sourceMappingURL=mgt2.bundle.js.map