Files
foundryvtt-mgt2/mgt2.bundle.js

4057 lines
166 KiB
JavaScript
Raw Blame History

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