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; } data.items = []; let compendiumName = "foundryvtt-shadows-over-sol.skills"; if ( compendiumName ) { let skills = await SoSUtility.loadCompendium(compendiumName); data.items = data.items.concat( skills ); } compendiumName = "foundryvtt-shadows-over-sol.consequences"; if ( compendiumName ) { let consequences = await SoSUtility.loadCompendium(compendiumName) data.items = data.items.concat(consequences); } return super.create(data, options); } /* -------------------------------------------- */ async prepareData() { super.prepareData(); if ( !this.cardDeck ) { if ( this.hasPlayerOwner) { this.cardDeck = new SoSCardDeck(); this.cardDeck.initCardDeck( this, this.data.data.internals.deck ); } else { this.cardDeck = game.system.sos.gmDeck; } } this.controlScores(); } /* -------------------------------------------- */ 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), critical: this.data.data.stats.speed.value + this.data.data.stats.perception.value + this.data.data.stats.dexterity.value } } /* -------------------------------------------- */ getEdge( ) { return this.data.data.scores.edge.value; } /* -------------------------------------------- */ getEncumbrance( ) { return this.data.data.scores.encumbrance.value; } /* -------------------------------------------- */ computeEdge( ) { return Math.ceil( (this.data.data.stats.intelligence.value + this.data.data.stats.charisma.value) / 2) + this.data.data.scores.edge.bonus; } /* -------------------------------------------- */ 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); } /* -------------------------------------------- */ 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); } /* -------------------------------------------- */ 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.updateEmbeddedEntity("OwnedItem", 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.updateEmbeddedEntity("OwnedItem", 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.data.data.stats.strength.value }); } // 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.updateEmbeddedEntity("OwnedItem", 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.updateEmbeddedEntity("OwnedItem", 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() ), 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), skill: duplicate(skill), actor: this, modifierList: SoSUtility.fillRange(-10, +10), tnList: SoSUtility.fillRange(6, 20), malusConsequence: 0, bonusConsequence: 0, woundMalus: 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() ), 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 } 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 applyWounds( flipData ) { 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; } 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(); } }