445 lines
17 KiB
JavaScript
445 lines
17 KiB
JavaScript
import { SoSCardDeck } from "./sos-card-deck.js";
|
|
import { SoSUtility } from "./sos-utility.js";
|
|
import { SoSFlipDialog } from "./sos-flip-dialog.js";
|
|
|
|
|
|
/* -------------------------------------------- */
|
|
/**
|
|
* Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system.
|
|
* @extends {Actor}
|
|
*/
|
|
export class SoSActor extends Actor {
|
|
|
|
/* -------------------------------------------- */
|
|
/**
|
|
* Override the create() function to provide additional SoS functionality.
|
|
*
|
|
* This overrided create() function adds initial items
|
|
* Namely: Basic skills, money,
|
|
*
|
|
* @param {Object} data Barebones actor data which this function adds onto.
|
|
* @param {Object} options (Unused) Additional options which customize the creation workflow.
|
|
*
|
|
*/
|
|
|
|
static async create(data, options) {
|
|
|
|
// Case of compendium global import
|
|
if (data instanceof Array) {
|
|
return super.create(data, options);
|
|
}
|
|
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
|
|
if (data.items) {
|
|
let actor = super.create(data, options);
|
|
return actor;
|
|
}
|
|
|
|
let items = [];
|
|
let compendiumName = "foundryvtt-shadows-over-sol.skills";
|
|
if ( compendiumName ) {
|
|
let skills = await SoSUtility.loadCompendium(compendiumName);
|
|
items = skills.map(i => i.toObject());
|
|
}
|
|
compendiumName = "foundryvtt-shadows-over-sol.consequences";
|
|
if ( compendiumName ) {
|
|
let consequences = await SoSUtility.loadCompendium(compendiumName)
|
|
items = items.concat( consequences.map(i => i.toObject()) );
|
|
}
|
|
data.items = items;
|
|
console.log(data);
|
|
|
|
return super.create(data, options);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async prepareData() {
|
|
super.prepareData();
|
|
|
|
this.checkDeck();
|
|
this.controlScores();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
checkDeck() {
|
|
if ( !this.cardDeck && this.hasPlayerOwner ) {
|
|
this.cardDeck = new SoSCardDeck();
|
|
this.cardDeck.initCardDeck( this, this.data.data.internals.deck );
|
|
}
|
|
if ( !this.hasPlayerOwner ) {
|
|
this.cardDeck = game.system.sos.gmDeck.GMdeck;
|
|
console.log("DECK : ", this.cardDeck);
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
getDeckSize() {
|
|
return this.cardDeck.getDeckSize();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
getEdgesCard( ) {
|
|
let edgesCard = duplicate(this.cardDeck.data.cardEdge);
|
|
for (let edge of edgesCard) {
|
|
edge.path = `systems/foundryvtt-shadows-over-sol/img/cards/${edge.cardName}.webp`
|
|
}
|
|
return edgesCard;
|
|
}
|
|
/* -------------------------------------------- */
|
|
resetDeckFull( ) {
|
|
this.cardDeck.shuffleDeck();
|
|
this.cardDeck.drawEdge( this.data.data.scores.edge.value );
|
|
this.saveDeck();
|
|
}
|
|
/* -------------------------------------------- */
|
|
drawNewEdge( ) {
|
|
this.cardDeck.drawEdge( 1 );
|
|
this.saveDeck();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
discardEdge( cardName ) {
|
|
this.cardDeck.discardEdge( cardName );
|
|
this.saveDeck();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
resetDeck( ) {
|
|
this.cardDeck.resetDeck();
|
|
this.saveDeck();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
saveDeck( ) {
|
|
let deck = { deck: duplicate(this.cardDeck.data.deck),
|
|
discard: duplicate(this.cardDeck.data.discard),
|
|
cardEdge: duplicate(this.cardDeck.data.cardEdge)
|
|
}
|
|
if ( this.hasPlayerOwner ) {
|
|
this.update( { 'data.internals.deck': deck });
|
|
} else {
|
|
game.settings.set("foundryvtt-shadows-over-sol", "gmDeck", deck );
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
getDefense( ) {
|
|
return this.data.data.scores.defense;
|
|
}
|
|
/* -------------------------------------------- */
|
|
computeDefense() {
|
|
return { value: Math.ceil((this.data.data.stats.speed.value + this.data.data.stats.perception.value + this.data.data.stats.dexterity.value) / 2) + this.data.data.scores.defense.bonusmalus,
|
|
critical: this.data.data.stats.speed.value + this.data.data.stats.perception.value + this.data.data.stats.dexterity.value + this.data.data.scores.defense.bonusmalus
|
|
}
|
|
}
|
|
/* -------------------------------------------- */
|
|
getEdge( ) {
|
|
return this.data.data.scores.edge.value;
|
|
}
|
|
/* -------------------------------------------- */
|
|
getEncumbrance( ) {
|
|
return this.data.data.scores.encumbrance.value;
|
|
}
|
|
computeEncumbrance( ) {
|
|
return this.data.data.stats.strength.value + this.data.data.scores.encumbrance.bonusmalus;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
computeEdge( ) {
|
|
return Math.ceil( (this.data.data.stats.intelligence.value + this.data.data.stats.charisma.value) / 2) + this.data.data.scores.edge.bonusmalus;
|
|
}
|
|
/* -------------------------------------------- */
|
|
getShock( ) {
|
|
return this.data.data.scores.shock.value;
|
|
}
|
|
computeShock() {
|
|
return Math.ceil( this.data.data.stats.endurance.value + this.data.data.stats.determination.value + this.data.data.scores.dr.value) + this.data.data.scores.shock.bonusmalus;
|
|
}
|
|
/* -------------------------------------------- */
|
|
getWound( ) {
|
|
return this.data.data.scores.wound.value;
|
|
}
|
|
computeWound() {
|
|
return Math.ceil( (this.data.data.stats.strength.value + this.data.data.stats.endurance.value) / 2) + this.data.data.scores.wound.bonusmalus;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
getSkillExperience( skillName ) {
|
|
return this.data.items.filter( item => item.type == 'skillexperience' && item.data.skill == skillName);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async wornObject( itemID) {
|
|
let item = this.getOwnedItem(itemID);
|
|
if (item && item.data.data) {
|
|
let update = { _id: item.id, "data.worn": !item.data.data.worn };
|
|
await this.updateEmbeddedDocuments("Item", [update]);
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async equipObject(itemID) {
|
|
let item = this.getOwnedItem(itemID);
|
|
if (item && item.data.data) {
|
|
let update = { _id: item.id, "data.equiped": !item.data.data.equiped };
|
|
await this.updateEmbeddedDocuments("Item", [update]);
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async controlScores() {
|
|
// Defense check
|
|
let defenseData = this.getDefense();
|
|
let newDefenseData = this.computeDefense();
|
|
if ( defenseData.value != newDefenseData.value || defenseData.critical != newDefenseData.critical) {
|
|
await this.update( {'data.scores.defense': newDefenseData});
|
|
}
|
|
// Edge check
|
|
if ( this.getEdge() != this.computeEdge() ) {
|
|
await this.update( {'data.scores.edge.value': this.computeEdge()});
|
|
}
|
|
// Encumbrance
|
|
if ( this.getEncumbrance() != this.data.data.stats.strength.value ) {
|
|
await this.update( {'data.scores.encumbrance.value': this.computeEncumbrance() });
|
|
}
|
|
// Shock
|
|
if ( this.getShock() != this.computeShock() ) {
|
|
await this.update( {'data.scores.shock.value': this.computeShock() });
|
|
}
|
|
// Wounds
|
|
if ( this.getWound() != this.computeWound() ) {
|
|
await this.update( {'data.scores.wound.value': this.computeWound() });
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async updateWound(woundName, value) {
|
|
let wounds = duplicate(this.data.data.wounds)
|
|
wounds[woundName] = value;
|
|
await this.update( { 'data.wounds': wounds } );
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async updateSkill(skillName, value) {
|
|
let skill = this.data.items.find( item => item.name == skillName);
|
|
if (skill) {
|
|
const update = { _id: skill.id, 'data.value': value };
|
|
const updated = await this.updateEmbeddedDocuments("Item", [ update] ); // Updates one EmbeddedEntity
|
|
}
|
|
}
|
|
/* -------------------------------------------- */
|
|
async updateSkillExperience(skillName, value) {
|
|
let skill = this.data.items.find( item => item.name == skillName);
|
|
if (skill) {
|
|
const update = { _id: skill.id, 'data.xp': value };
|
|
const updated = await this.updateEmbeddedDocuments("Item", [update]); // Updates one EmbeddedEntity
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
getApplicableConsequences( ) {
|
|
let consequences = this.data.items.filter( item => item.type == 'consequence' && item.data.severity != 'none');
|
|
return consequences;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async rollStat( statKey ) {
|
|
|
|
let flipData = {
|
|
mode: 'stat',
|
|
stat: duplicate(this.data.data.stats[statKey]),
|
|
actor: this,
|
|
modifierList: SoSUtility.fillRange(-10, +10),
|
|
tnList: SoSUtility.fillRange(6, 20),
|
|
consequencesList: duplicate( this.getApplicableConsequences() ),
|
|
weaknessList: this.data.items.filter( item => item.type == 'weakness' ),
|
|
wounds: duplicate( this.data.data.wounds),
|
|
malusConsequence: 0,
|
|
bonusConsequence: 0,
|
|
woundMalus: 0
|
|
}
|
|
let html = await renderTemplate('systems/foundryvtt-shadows-over-sol/templates/dialog-flip.html', flipData);
|
|
new SoSFlipDialog(flipData, html).render(true);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async rollSkill( skill ) {
|
|
let flipData = {
|
|
mode: 'skill',
|
|
statList: duplicate(this.data.data.stats),
|
|
selectedStat: 'strength',
|
|
consequencesList: duplicate( this.getApplicableConsequences() ),
|
|
wounds: duplicate( this.data.data.wounds),
|
|
skillExperienceList: this.getSkillExperience( skill.name),
|
|
skill: duplicate(skill),
|
|
actor: this,
|
|
modifierList: SoSUtility.fillRange(-10, +10),
|
|
tnList: SoSUtility.fillRange(6, 20),
|
|
malusConsequence: 0,
|
|
bonusConsequence: 0,
|
|
woundMalus: 0,
|
|
bonusSkillXP: 0
|
|
}
|
|
flipData.statList['nostat'] = { label: "No stat (ie defaulting skills)", value: 0, cardsuit: "none" }
|
|
let html = await renderTemplate('systems/foundryvtt-shadows-over-sol/templates/dialog-flip.html', flipData);
|
|
new SoSFlipDialog(flipData, html).render(true);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async rollWeapon( weapon ) {
|
|
let target = SoSUtility.getTarget();
|
|
let skill, selectedStatName;
|
|
if ( weapon.data.data.category == 'ballistic' || weapon.data.data.category == 'laser' ) {
|
|
skill = this.data.items.find( item => item.name == 'Guns');
|
|
selectedStatName = 'dexterity';
|
|
} else if ( weapon.data.data.category == 'melee' ) {
|
|
skill = this.data.items.find( item => item.name == 'Melee');
|
|
selectedStatName = 'dexterity';
|
|
} else if ( weapon.data.data.category == 'grenade' ) {
|
|
skill = this.data.items.find( item => item.name == 'Athletics');
|
|
selectedStatName = 'dexterity';
|
|
}
|
|
|
|
let flipData = {
|
|
mode: 'weapon',
|
|
weapon: duplicate(weapon.data),
|
|
statList: duplicate(this.data.data.stats),
|
|
target: target,
|
|
selectedStat: selectedStatName,
|
|
consequencesList: duplicate( this.getApplicableConsequences() ),
|
|
skillExperienceList: this.getSkillExperience( skill.name),
|
|
wounds: duplicate( this.data.data.wounds),
|
|
skill: duplicate(skill),
|
|
actor: this,
|
|
modifierList: SoSUtility.fillRange(-10, +10),
|
|
tnList: SoSUtility.fillRange(6, 20),
|
|
malusConsequence: 0,
|
|
bonusConsequence: 0,
|
|
woundMalus: 0,
|
|
bonusSkillXP: 0
|
|
}
|
|
|
|
console.log(flipData);
|
|
|
|
flipData.statList['nostat'] = { label: "No stat (ie defaulting skills)", value: 0, cardsuit: "none" }
|
|
let html = await renderTemplate('systems/foundryvtt-shadows-over-sol/templates/dialog-flip.html', flipData);
|
|
new SoSFlipDialog(flipData, html).render(true);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async checkDeath( ) {
|
|
if ( this.data.data.scores.currentwounds.value >= this.data.data.scores.wound.value*2) {
|
|
let woundData = {
|
|
name: this.name,
|
|
wounds: this.data.data.wounds,
|
|
currentWounds: this.data.data.scores.currentwounds.value,
|
|
totalWounds: this.data.data.scores.wound.value
|
|
}
|
|
let html = await renderTemplate('systems/foundryvtt-shadows-over-sol/templates/chat-character-death.html', woundData );
|
|
ChatMessage.create( { content: html, whisper: [ChatMessage.getWhisperRecipients(this.name), ChatMessage.getWhisperRecipients("GM") ] } );
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
computeCurrentWounds( ) {
|
|
let wounds = this.data.data.wounds;
|
|
return wounds.light + (wounds.moderate*2) + (wounds.severe*3) + (wounds.critical*4);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async applyConsequenceWound( severity, consequenceName) {
|
|
if ( severity == 'none') return; // Nothing !
|
|
|
|
let wounds = duplicate(this.data.data.wounds);
|
|
if (severity == 'light' ) wounds.light += 1;
|
|
if (severity == 'moderate' ) wounds.moderate += 1;
|
|
if (severity == 'severe' ) wounds.severe += 1;
|
|
if (severity == 'critical' ) wounds.critical += 1;
|
|
|
|
let sumWound = wounds.light + (wounds.moderate*2) + (wounds.severe*3) + (wounds.critical*4);
|
|
let currentWounds = duplicate(this.data.data.scores.currentwounds);
|
|
currentWounds.value = sumWound;
|
|
await this.update( { 'data.scores.currentwounds': currentWounds, 'data.wounds': wounds } );
|
|
|
|
let woundData = {
|
|
name: this.name,
|
|
consequenceName: consequenceName,
|
|
severity: severity,
|
|
wounds: wounds,
|
|
currentWounds: sumWound,
|
|
totalWounds: this.data.data.scores.wound.value
|
|
}
|
|
let html = await renderTemplate('systems/foundryvtt-shadows-over-sol/templates/chat-damage-consequence.html', woundData );
|
|
ChatMessage.create( { content: html, whisper: ChatMessage.getWhisperRecipients(this.name).concat(ChatMessage.getWhisperRecipients("GM")) } );
|
|
|
|
this.checkDeath();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async addObjectToContainer( itemId, containerId ) {
|
|
let container = this.data.items.find( item => item._id == containerId && item.type == 'container');
|
|
let object = this.data.items.find( item => item._id == itemId );
|
|
if ( container ) {
|
|
if ( object.type == 'container') {
|
|
ui.notifications.warn("Only 1 level of container... sorry");
|
|
return
|
|
}
|
|
let alreadyInside = this.data.items.filter( item => item.data.containerid && item.data.containerid == containerId);
|
|
if ( alreadyInside.length >= container.data.container ) {
|
|
ui.notifications.warn("Container is already full !");
|
|
} else {
|
|
await this.updateOwnedItem( { _id: object.id, 'data.containerid':containerId });
|
|
}
|
|
} else if ( object && object.data.containerid) { // remove from container
|
|
await this.updateOwnedItem( { _id: object.id, 'data.containerid':"" });
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async applyWounds( flipData ) {
|
|
if ( flipData.damageStatus == 'no_damage') {
|
|
let html = await renderTemplate('systems/foundryvtt-shadows-over-sol/templates/chat-nodamage-taken.html', flipData );
|
|
ChatMessage.create( { content: html, whisper: [ChatMessage.getWhisperRecipients(this.name), ChatMessage.getWhisperRecipients("GM") ] } );
|
|
return;
|
|
}
|
|
|
|
let wounds = duplicate(this.data.data.wounds);
|
|
for (let wound of flipData.woundsList ) {
|
|
if (wound == 'L' ) wounds.light += 1;
|
|
if (wound == 'M' ) wounds.moderate += 1;
|
|
if (wound == 'S' ) wounds.severe += 1;
|
|
if (wound == 'C' ) wounds.critical += 1;
|
|
}
|
|
// Compute total
|
|
let sumWound = wounds.light + (wounds.moderate*2) + (wounds.severe*3) + (wounds.critical*4);
|
|
let currentWounds = duplicate(this.data.data.scores.currentwounds);
|
|
currentWounds.value = sumWound;
|
|
if ( sumWound >= this.data.data.scores.wound.value) {
|
|
let bleeding = this.data.items.find( item => item.type == 'consequence' && item.name == 'Bleeding');
|
|
let newSeverity = SoSUtility.increaseConsequenceSeverity( bleeding.severity );
|
|
await this.updateOwnedItem( { _id: bleeding.id, 'data.severity': newSeverity});
|
|
flipData.isBleeding = newSeverity;
|
|
}
|
|
// Stun consequence
|
|
if ( flipData.nbStun > 0) {
|
|
let stun = this.data.items.find( item => item.type == 'consequence' && item.name == 'Sun');
|
|
let newSeverity = stun.severity;
|
|
for(i=0; i<flipData.nbStun; i++) {
|
|
newSeverity = SoSUtility.increaseConsequenceSeverity( newSeverity );
|
|
}
|
|
await this.updateOwnedItem( { _id: stun.id, 'data.severity': newSeverity});
|
|
flipData.isStun = newSeverity;
|
|
}
|
|
await this.update( { 'data.scores.currentwounds': currentWounds, 'data.wounds': wounds } );
|
|
|
|
flipData.defenderName = this.name;
|
|
flipData.wounds = wounds;
|
|
flipData.currentWounds = sumWound;
|
|
flipData.totalWounds = this.data.data.scores.wound.value;
|
|
let html = await renderTemplate('systems/foundryvtt-shadows-over-sol/templates/chat-damage-taken.html', flipData );
|
|
ChatMessage.create( { content: html, whisper: [ChatMessage.getWhisperRecipients(this.name), ChatMessage.getWhisperRecipients("GM") ] } );
|
|
|
|
this.checkDeath();
|
|
}
|
|
|
|
}
|