master
U~man 2020-06-28 16:31:47 +02:00
parent d832e9ab22
commit ad5f2ccd85
7 changed files with 2 additions and 515 deletions

Binary file not shown.

BIN
package/ose-v0.1.zip Normal file

Binary file not shown.

View File

@ -1,5 +1,4 @@
import { OseActor } from "./entity.js";
import { ActorTraitSelector } from "../apps/trait-selector.js";
/**
* Extend the basic ActorSheet with some very simple modifications
@ -40,7 +39,7 @@ export class OseActorSheetMonster extends ActorSheet {
*/
getData() {
const data = super.getData();
data.config = CONFIG.MAJI;
data.config = CONFIG.OSE;
// Prepare owned items
this._prepareItems(data);
@ -52,18 +51,6 @@ export class OseActorSheetMonster extends ActorSheet {
* @private
*/
_prepareItems(data) {
let [traits, techniques] = data.items.reduce(
(arr, item) => {
// Classify items into types
if (item.type === "feature") arr[0].push(item);
else if (item.type === "technique") arr[1].push(item);
return arr;
},
[[], [], [], []]
);
// Assign and return
data.traits = traits;
data.techniques = techniques;
}
_onItemSummary(event) {
@ -91,70 +78,6 @@ export class OseActorSheetMonster extends ActorSheet {
}
}
/**
* Handle spawning the ActorTraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onTraitSelector(event) {
event.preventDefault();
const a = event.currentTarget;
const options = {
name: `data.${a.dataset.target}`,
title: a.innerText,
choices: CONFIG.MAJI[a.dataset.options],
};
new ActorTraitSelector(this.actor, options).render(true);
}
_keyUpHandler(event) {
if (event.keyCode == 17) {
let icons = document.querySelectorAll(".monster .roll-empowerable");
for (let i = 0; i < icons.length; i++) {
let icon = icons[i].getElementsByTagName("img")[0];
if (icon.getAttribute("src") == "/systems/majimonsters/assets/icons/dice/d6s.png") {
icon.setAttribute("src", "/systems/majimonsters/assets/icons/dice/d8s.png");
} else if (icon.getAttribute("src") == "/systems/majimonsters/assets/icons/dice/d.png"){
icon.setAttribute("src", "/systems/majimonsters/assets/icons/dice/dp.png");
}
}
}
}
_keyDownHandler(event) {
if (event.keyCode == 17) {
let icons = document.querySelectorAll(".monster .roll-empowerable");
for (let i = 0; i < icons.length; i++) {
let icon = icons[i].getElementsByTagName("img")[0];
if (icon.getAttribute("src") == "/systems/majimonsters/assets/icons/dice/d8s.png") {
icon.setAttribute("src", "/systems/majimonsters/assets/icons/dice/d6s.png");
} else if (icon.getAttribute("src") == "/systems/majimonsters/assets/icons/dice/dp.png"){
icon.setAttribute("src", "/systems/majimonsters/assets/icons/dice/d.png");
}
}
}
}
_onRollTechnique(event) {
event.preventDefault();
let itemId = event.currentTarget.parentElement.previousElementSibling.dataset.itemId;
const technique = this.actor.getOwnedItem(itemId);
return technique.roll({empowered: event.ctrlKey, type: event.currentTarget.dataset.rollType});
}
_onRollStat(event) {
event.preventDefault();
let stat = event.currentTarget.parentElement.dataset.attribute;
this.actor.rollStat(stat, { event: event, empowered: event.ctrlKey });
}
_onShowCard(event) {
event.preventDefault();
let itemId = event.currentTarget.closest(".item").dataset.itemId;
const technique = this.actor.getOwnedItem(itemId);
return technique.roll({empowered: event.ctrlKey, type: event.currentTarget.dataset.rollType});
}
/* -------------------------------------------- */
/**
@ -196,60 +119,7 @@ export class OseActorSheetMonster extends ActorSheet {
this._onItemSummary(event);
});
// Switch Gender
html.find(".mm_gender").click((event) => {
event.preventDefault();
if (this.actor.data["data"].details.gender === "female") {
this.actor.data["data"].details.gender = "male";
this.render();
return;
}
this.actor.data["data"].details.gender = "female";
this.render();
});
// Config modifier
html.find(".mm_bullet").click((ev) => {
event.preventDefault();
let attribute = $(ev.currentTarget).siblings(".modifier");
if (attribute.hasClass("hidden")) {
attribute.removeClass("hidden");
return;
}
attribute.addClass("hidden");
});
// trait Selector
html.find(".trait-selector").click(this._onTraitSelector.bind(this));
// Listen to event preventing duplicate bindings
if (document.getElementsByClassName("monster").length == 1) {
document.addEventListener("keydown", this._keyUpHandler);
document.addEventListener("keyup", this._keyDownHandler);
}
html.find(".roll-icon").click((event) => {
if (event.currentTarget.classList.contains("roll-technique")) {
this._onRollTechnique(event);
return;
}
this._onRollStat(event);
});
html.find(".item-show").click((event) => {
this._onShowCard(event);
});
// Handle default listeners last so system listeners are triggered first
super.activateListeners(html);
}
/** @override */
async close(options) {
if (document.getElementsByClassName("monster").length == 1) {
document.removeEventListener("keydown", this._keyUpHandler);
document.removeEventListener("keyup", this._keyDownHandler);
}
return super.close(options);
}
}

View File

@ -1,66 +0,0 @@
/**
* A specialized form used to select damage or condition types which apply to an Actor
* @type {FormApplication}
*/
export class ActorTraitSelector extends FormApplication {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
id: "trait-selector",
classes: ["maji"],
title: "Actor Trait Selection",
template: "systems/ose/templates/apps/trait-selector.html",
width: 320,
height: "auto",
choices: {},
});
}
/* -------------------------------------------- */
/**
* Return a reference to the target attribute
* @type {String}
*/
get attribute() {
return this.options.name;
}
/* -------------------------------------------- */
/**
* Provide data to the HTML template for rendering
* @type {Object}
*/
getData() {
// Get current values
let attr = getProperty(this.object.data, this.attribute);
// Populate choices
const choices = duplicate(this.options.choices);
for (let [k, v] of Object.entries(choices)) {
choices[k] = {
label: v,
chosen: attr.includes(k),
};
}
// Return data
return { choices: choices };
}
/* -------------------------------------------- */
/**
* Update the Actor object with new trait data processed from the form
* @private
*/
_updateObject(event, formData) {
const choices = [];
for (let [k, v] of Object.entries(formData)) {
if (v) {
choices.push(k);
}
}
this.object.update({[`${this.attribute}`]: choices});
}
}

View File

@ -12,245 +12,4 @@ export class OseItem extends Item {
prepareData() {
super.prepareData();
}
static async create(data, options = {}) {
return super.create(data, options);
}
/* -------------------------------------------- */
/** @override */
async update(data, options = {}) {
return super.update(data, options);
}
/* -------------------------------------------- */
static chatListeners(html) {
}
/**
* Roll the item to Chat
* @return {Promise}
*/
async roll(options = {}) {
if (options.type == "chat") {
this.rollCard(options);
return;
}
if (options.type == "attack") {
this.rollAttack(options);
return;
}
if (options.type == "damage") {
this.rollDamage(options);
return;
}
if (options.type == "trigger") {
return;
}
}
/* -------------------------------------------- */
/* Item Rolls - Attack, Damage, Saves, Checks */
/* -------------------------------------------- */
async rollCard(options = {}) {
// Basic template rendering data
const token = this.actor.token;
const templateData = {
actor: this.actor,
tokenId: token ? `${token.scene._id}.${token.id}` : null,
item: this.data,
hasAttack: this.data.data.details.attack != "",
isHealing: this.data.data.tags.descriptor == "healing",
hasDamage: this.data.data.damage.die != 0,
hasTrigger: this.data.data.trigger.threshold != 0,
config: CONFIG.MAJI,
};
// Render the chat card
const template = "systems/ose/templates/chat/technique-card.html";
const html = await renderTemplate(template, templateData);
// Basic chat message data
const chatData = {
user: game.user._id,
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
content: html,
speaker: {
actor: this.actor._id,
token: this.actor.token,
alias: this.actor.name,
},
};
// Toggle default roll mode
let rollMode = game.settings.get("core", "rollMode");
if (["gmroll", "blindroll"].includes(rollMode))
chatData["whisper"] = ChatMessage.getWhisperIDs("GM");
if (rollMode === "blindroll") chatData["blind"] = true;
// Create the chat message
return ChatMessage.create(chatData);
}
/**
* Place an attack roll using an item (weapon, feat, spell, or equipment)
* Rely upon the Dice5e.d20Roll logic for the core implementation
*
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
*/
rollAttack(options = {}) {
const itemData = this.data.data;
const actorData = this.actor.data.data;
if (this.type != "technique") return;
const label = this.name;
let parts = [];
if (options.empowered) {
parts.push("2d8");
} else {
parts.push("2d6");
}
parts.push(
actorData.attributes[itemData.details.attack].value +
actorData.attributes[itemData.details.attack].mod
);
let rollMode = game.settings.get("core", "rollMode");
let roll = new Roll(parts.join(" + "), {}).roll();
roll.toMessage(
{
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor: `${label} ${game.i18n.localize("MAJI.technique.attack")}`,
},
{ rollMode }
);
return roll;
}
rollDamage(options = {}) {
const itemData = this.data.data;
const actorData = this.actor.data.data;
if (this.type != "technique") return;
const label = this.name;
let parts = [];
if (itemData.damage.num && itemData.damage.die) {
parts.push(`${itemData.damage.num}d${itemData.damage.die}`);
}
if (itemData.damage.bonus) {
parts.push(
actorData.attributes[itemData.damage.bonus].value +
actorData.attributes[itemData.damage.bonus].mod
);
}
if (options.empowered && itemData.damage.die) {
parts.push(`1d${itemData.damage.die}`);
}
if (actorData.affinities.includes(itemData.element)) {
parts.push(actorData.affinity.value + actorData.affinity.mod);
}
let rollMode = game.settings.get("core", "rollMode");
let roll = new Roll(parts.join(" + "), {}).roll();
roll.toMessage(
{
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor: `${label} ${game.i18n.localize("MAJI.technique.damage")}`,
},
{ rollMode }
);
return roll;
}
/* -------------------------------------------- */
/**
* Handle toggling the visibility of chat card content when the name is clicked
* @param {Event} event The originating click event
* @private
*/
static _onChatCardToggleContent(event) {
event.preventDefault();
const header = event.currentTarget;
const card = header.closest(".chat-card");
const content = card.querySelector(".card-content");
content.style.display = content.style.display === "none" ? "block" : "none";
}
static async _onChatCardAction(event) {
event.preventDefault();
// Extract card data
const button = event.currentTarget;
button.disabled = true;
const card = button.closest(".chat-card");
const messageId = card.closest(".message").dataset.messageId;
const message = game.messages.get(messageId);
const action = button.dataset.action;
// Validate permission to proceed with the roll
const isTargetted = action === "trigger";
if (!(isTargetted || game.user.isGM || message.isAuthor)) return;
// Get the Actor from a synthetic Token
const actor = this._getChatCardActor(card);
if (!actor) return;
// Get the Item
const item = actor.getOwnedItem(card.dataset.itemId);
// Get card targets
let targets = [];
if (isTargetted) {
targets = this._getChatCardTargets(card);
if (!targets.length) {
ui.notifications.warn(
`You must have one or more controlled Tokens in order to use this option.`
);
return (button.disabled = false);
}
}
// Attack and Damage Rolls
if (action === "attack") await item.rollAttack({ event });
// Saving Throws for card targets
else if (action === "trigger") {
for (let t of targets) {
await t.rollTrigger(
{
threshold: button.dataset.threshold,
condition: button.dataset.condition,
},
{ event }
);
}
}
// Re-enable the button
button.disabled = false;
}
static _getChatCardActor(card) {
// Case 1 - a synthetic actor from a Token
const tokenKey = card.dataset.tokenId;
if (tokenKey) {
const [sceneId, tokenId] = tokenKey.split(".");
const scene = game.scenes.get(sceneId);
if (!scene) return null;
const tokenData = scene.getEmbeddedEntity("Token", tokenId);
if (!tokenData) return null;
const token = new Token(tokenData);
return token.actor;
}
// Case 2 - use Actor ID directory
const actorId = card.dataset.actorId;
return game.actors.get(actorId) || null;
}
static _getChatCardTargets(card) {
const character = game.user.character;
const controlled = canvas.tokens.controlled;
const targets = controlled.reduce(
(arr, t) => (t.actor ? arr.concat([t.actor]) : arr),
[]
);
if (character && controlled.length === 0) targets.push(character);
return targets;
}
}

View File

@ -45,23 +45,10 @@ export class OseItemSheet extends ItemSheet {
*/
getData() {
const data = super.getData();
data.config = CONFIG.MAJI;
data.config = CONFIG.OSE;
return data;
}
_onDrop(event) {
event.preventDefault();
let data;
try {
data = JSON.parse(event.dataTransfer.getData("text/plain"));
} catch (err) {
return;
}
// Only handle Actor drops
if (data.type !== "Actor") return;
this.entity.update({ data: { monster: { id: data.id } } });
}
/* -------------------------------------------- */
/**
@ -69,30 +56,6 @@ export class OseItemSheet extends ItemSheet {
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) {
if (this.item.type == "drajule") {
const bar = html.find(".monster-drop");
bar[0].ondrop = this._onDrop.bind(this);
}
html.find(".entity").click(event => {
event.preventDefault();
const element = event.currentTarget;
const entityId = element.dataset.entityId;
const entity = game.actors.entities.find(f => f.id === entityId);
const sheet = entity.sheet;
if (sheet._minimized) return sheet.maximize();
else return sheet.render(true);
});
super.activateListeners(html);
}
/* -------------------------------------------- */
/**
* Implement the _updateObject method as required by the parent class spec
* This defines how to update the subject of the form when the form is submitted
* @private
*/
_updateObject(event, formData) {
return this.object.update(formData);
}
}

View File

@ -1,39 +0,0 @@
<form class="{{cssClass}}" autocomplete="off">
<div class="flexrow mm_part1">
{{> "systems/ose/templates/actors/partials/monster-header.html"}}
</div>
{{! Navigation }}
<div class="flex flexrow">
{{! Sheet Tab Navigation }}
<nav class="sheet-tabs tabs" data-group="primary">
<a class="item" data-tab="notes">
<span>{{localize "MAJI.category.notes"}}</span>
</a>
<a class="item" data-tab="stats">
<span>{{localize "MAJI.category.stats"}}</span>
</a>
<a class="item" data-tab="traits">
<span>{{localize "MAJI.category.traits"}}</span>
</a>
<a class="item" data-tab="techniques">
<span>{{localize "MAJI.category.techniques"}}</span>
</a>
</nav>
<section class="sheet-body">
<div class="mm_mmtabs tab" data-group="primary" data-tab="notes">
{{editor content=data.details.biography target="data.details.biography"
button=true owner=owner editable=editable}}
</div>
{{! Attributes }}
<div class="mm_mmtabs tab" data-group="primary" data-tab="stats">
{{> "systems/ose/templates/actors/partials/monster-attributes-tab.html"}}
</div>
<div class="mm_mmtabs tab" data-group="primary" data-tab="traits">
{{> "systems/ose/templates/actors/partials/monster-traits-tab.html"}}
</div>
<div class="mm_mmtabs tab" data-group="primary" data-tab="techniques">
{{> "systems/ose/templates/actors/partials/monster-techniques-tab.html"}}
</div>
</section>
</div>
</form>