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.system.cardDeck && this.hasPlayerOwner ) { this.system.cardDeck = new SoSCardDeck(); this.system.cardDeck.initCardDeck( this, this.system.internals.deck ); } if ( !this.hasPlayerOwner ) { this.system.cardDeck = game.system.sos.gmDeck.GMdeck; console.log("DECK : ", this.system.cardDeck); } } /* -------------------------------------------- */ getDeckSize() { return this.system.cardDeck.getDeckSize(); } /* -------------------------------------------- */ getEdgesCard( ) { let edgesCard = foundry.utils.duplicate(this.system.cardDeck.data.cardEdge); for (let edge of edgesCard) { edge.path = `systems/foundryvtt-shadows-over-sol/img/cards/${edge.cardName}.webp` } return edgesCard; } /* -------------------------------------------- */ resetDeckFull( ) { this.system.cardDeck.shuffleDeck(); this.system.cardDeck.drawEdge( this.system.scores.edge.value ); this.saveDeck(); } /* -------------------------------------------- */ drawNewEdge( ) { this.system.cardDeck.drawEdge( 1 ); this.saveDeck(); } /* -------------------------------------------- */ discardEdge( cardName ) { this.system.cardDeck.discardEdge( cardName ); this.saveDeck(); } /* -------------------------------------------- */ resetDeck( ) { this.system.cardDeck.resetDeck(); this.saveDeck(); } /* -------------------------------------------- */ saveDeck( ) { let deck = { deck: foundry.utils.duplicate(this.system.cardDeck.data.deck), discard: foundry.utils.duplicate(this.system.cardDeck.data.discard), cardEdge: foundry.utils.duplicate(this.system.cardDeck.data.cardEdge) } if ( this.hasPlayerOwner ) { this.update( { 'system.internals.deck': deck }); } else { game.settings.set("foundryvtt-shadows-over-sol", "gmDeck", deck ); } } /* -------------------------------------------- */ getDefense( ) { return this.system.scores.defense; } /* -------------------------------------------- */ computeDefense() { return { value: Math.ceil((this.system.stats.speed.value + this.system.stats.perception.value + this.system.stats.dexterity.value) / 2) + this.system.scores.defense.bonusmalus, critical: this.system.stats.speed.value + this.system.stats.perception.value + this.system.stats.dexterity.value + this.system.scores.defense.bonusmalus } } /* -------------------------------------------- */ getEdge( ) { return this.system.scores.edge.value; } /* -------------------------------------------- */ getEncumbrance( ) { return this.system.scores.encumbrance.value; } computeEncumbrance( ) { return this.system.stats.strength.value + this.system.scores.encumbrance.bonusmalus; } /* -------------------------------------------- */ computeEdge( ) { return Math.ceil( (this.system.stats.intelligence.value + this.system.stats.charisma.value) / 2) + this.system.scores.edge.bonusmalus; } /* -------------------------------------------- */ getShock( ) { return this.system.scores.shock.value; } computeShock() { return Math.ceil( this.system.stats.endurance.value + this.system.stats.determination.value + this.system.scores.dr.value) + this.system.scores.shock.bonusmalus; } /* -------------------------------------------- */ getWound( ) { return this.system.scores.wound.value; } computeWound() { return Math.ceil( (this.system.stats.strength.value + this.system.stats.endurance.value) / 2) + this.system.scores.wound.bonusmalus; } /* -------------------------------------------- */ getSkillExperience( skillName ) { return this.items.filter( item => item.type == 'skillexperience' && item.system.skill == skillName); } /* -------------------------------------------- */ async wornObject( itemID) { let item = this.items.get(itemID); if (item && item.system) { let update = { _id: item.id, "system.worn": !item.system.worn }; await this.updateEmbeddedDocuments("Item", [update]); } } /* -------------------------------------------- */ async equipObject(itemID) { let item = this.items.get(itemID) if (item && item.system) { let update = { _id: item.id, "system.equiped": !item.system.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.system.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 = foundry.utils.duplicate(this.system.wounds) wounds[woundName] = value; await this.update( { 'system.wounds': wounds } ); } /* -------------------------------------------- */ async updateSkill(skillName, value) { let skill = this.items.find( item => item.name == skillName); if (skill) { const update = { _id: skill.id, 'system.value': value }; const updated = await this.updateEmbeddedDocuments("Item", [ update] ); // Updates one EmbeddedEntity } } /* -------------------------------------------- */ async updateSkillExperience(skillName, value) { let skill = this.items.find( item => item.name == skillName); if (skill) { const update = { _id: skill.id, 'system.xp': value }; const updated = await this.updateEmbeddedDocuments("Item", [update]); // Updates one EmbeddedEntity } } /* -------------------------------------------- */ getApplicableConsequences( ) { let consequences = this.items.filter( item => item.type == 'consequence' && item.system.severity != 'none'); return consequences; } /* -------------------------------------------- */ async rollStat( statKey ) { let flipData = { mode: 'stat', stat: foundry.utils.duplicate(this.system.stats[statKey]), actor: this, modifierList: SoSUtility.fillRange(-10, +10), tnList: SoSUtility.fillRange(6, 20), consequencesList: foundry.utils.duplicate( this.getApplicableConsequences() ), weaknessList: this.items.filter( item => item.type == 'weakness' ), wounds: foundry.utils.duplicate( this.system.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: foundry.utils.duplicate(this.system.stats), selectedStat: 'strength', consequencesList: foundry.utils.duplicate( this.getApplicableConsequences() ), wounds: foundry.utils.duplicate( this.system.wounds), skillExperienceList: this.getSkillExperience( skill.name), skill: foundry.utils.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.system.category == 'ballistic' || weapon.system.category == 'laser' ) { skill = this.items.find( item => item.name == 'Guns'); selectedStatName = 'dexterity'; } else if ( weapon.system.category == 'melee' ) { skill = this.items.find( item => item.name == 'Melee'); selectedStatName = 'dexterity'; } else if ( weapon.system.category == 'grenade' ) { skill = this.items.find( item => item.name == 'Athletics'); selectedStatName = 'dexterity'; } let flipData = { mode: 'weapon', weapon: foundry.utils.duplicate(weapon), statList: foundry.utils.duplicate(this.system.stats), target: target, selectedStat: selectedStatName, consequencesList: foundry.utils.duplicate( this.getApplicableConsequences() ), skillExperienceList: this.getSkillExperience( skill.name), wounds: foundry.utils.duplicate( this.system.wounds), skill: foundry.utils.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.system.scores.currentwounds.value >= this.system.scores.wound.value*2) { let woundData = { name: this.name, wounds: this.system.wounds, currentWounds: this.system.scores.currentwounds.value, totalWounds: this.system.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).concat(ChatMessage.getWhisperRecipients("GM") ) } ) } } /* -------------------------------------------- */ computeCurrentWounds( ) { let wounds = this.system.wounds; return wounds.light + (wounds.moderate*2) + (wounds.severe*3) + (wounds.critical*4); } /* -------------------------------------------- */ async applyConsequenceWound( severity, consequenceName) { if ( severity == 'none') return; // Nothing ! let wounds = foundry.utils.duplicate(this.system.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 = foundry.utils.duplicate(this.system.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.system.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.items.find( item => item.id == containerId && item.type == 'container') let object = this.items.find( item => item.id == itemId ) console.log("Found", container, object) if ( container ) { if ( object.type == 'container') { ui.notifications.warn("Only 1 level of container... sorry"); return } let alreadyInside = this.items.filter( item => item.system.containerid && item.system.containerid == containerId); if ( alreadyInside.length >= container.system.container ) { ui.notifications.warn("Container is already full !"); } else { setTimeout(function() { this.updateEmbeddedDocuments( "Item", [{ _id: object.id, 'system.containerid':containerId }])}, 800 ) } } else if ( object && object.system.containerid) { // remove from container setTimeout(function() { this.updateEmbeddedDocuments( "Item", [{ _id: object.id, 'system.containerid':"" }])}, 800 ) } } /* -------------------------------------------- */ 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).concat(ChatMessage.getWhisperRecipients("GM")) } ); return; } let wounds = foundry.utils.duplicate(this.system.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 = foundry.utils.duplicate(this.system.scores.currentwounds); currentWounds.value = sumWound; if ( sumWound >= this.system.scores.wound.value) { let bleeding = this.items.find( item => item.type == 'consequence' && item.name == 'Bleeding'); let newSeverity = SoSUtility.increaseConsequenceSeverity( bleeding.system.severity ); await this.updateEmbeddedDocuments( "Item", [ { _id: bleeding.id, 'system.severity': newSeverity} ] ); flipData.isBleeding = newSeverity; } // Stun consequence if ( flipData.nbStun > 0) { let stun = this.items.find( item => item.type == 'consequence' && item.name == 'Stun'); let newSeverity = stun.system.severity; for(let i=0; i