diff --git a/src/module/config.ts b/src/module/config.ts index 61df102d7..b8ef8271c 100644 --- a/src/module/config.ts +++ b/src/module/config.ts @@ -92,6 +92,10 @@ const ROLLTYPES = Object.freeze({ Disadvantage: {key: 'Disadvantage', formula: "3d6kl2"} }); +const CONSUMABLES = Object.freeze([ + "air", "drugs", "food", "fuel", "magazine", "power_cell", "other" +]); + const DIFFICULTIES = Object.freeze({ CE: { Simple: {mod: 6, target: 2}, @@ -114,6 +118,7 @@ const DIFFICULTIES = Object.freeze({ export const TWODSIX = { CHARACTERISTICS: CHARACTERISTICS, + CONSUMABLES: CONSUMABLES, VARIANTS: VARIANTS, ROLLTYPES: ROLLTYPES, DIFFICULTIES: DIFFICULTIES, diff --git a/src/module/entities/TwodsixItem.ts b/src/module/entities/TwodsixItem.ts index 9efd2a178..82dad05e5 100644 --- a/src/module/entities/TwodsixItem.ts +++ b/src/module/entities/TwodsixItem.ts @@ -21,8 +21,38 @@ export default class TwodsixItem extends Item { if (this.getFlag("twodsix", "untrainedSkill")) { this.data.name = game.i18n.localize("TWODSIX.Actor.Skills.Untrained"); } + if (this.data.data.consumables) { + this.data.data.consumableData = this.data.data.consumables.map((consumableId:string) => { + // this is a bit hacky.. seems like the actor has not been initialized fully at this point. + return this.actor.data["items"].filter((item:TwodsixItem) => item._id === consumableId)[0]; + }); + this.data.data.consumableData.sort((a:TwodsixItem, b:TwodsixItem) => { + return ((a.name > b.name) ? -1 : ((a.name > b.name) ? 1 : 0)); + }); + } + } + + public async addConsumable(consumableId:string):Promise { + if (this.data.data.consumables.includes(consumableId)) { + console.error(`Twodsix | Consumable already exists for item ${this._id}`); + return; + } + await this.update({"data.consumables": this.data.data.consumables.concat(consumableId)}, {}); + } + + public async removeConsumable(consumableId: string):Promise { + const updatedConsumables = this.data.data.consumables.filter((cId:string) => { + return cId !== consumableId; + }); + const updateData = {"data.consumables": updatedConsumables}; + if (this.data.data.useConsumableForAttack === consumableId) { + updateData["data.useConsumableForAttack"] = ""; + } + await this.update(updateData, {}); } + //////// WEAPON //////// + public async performAttack(attackType:string, showThrowDialog:boolean, rateOfFireCE:number = null, showInChat = true):Promise { if (this.type !== "weapon") { return; @@ -34,26 +64,31 @@ export default class TwodsixItem extends Item { let numberOfAttacks = 1; let bonusDamage = "0"; - const rateOfFire = this.data.data.rateOfFire; - + const rof = parseInt(this.data.data.rateOfFire, 10); + const rateOfFire:number = rateOfFireCE ?? (!isNaN(rof) ? rof : 0); if (attackType && !rateOfFire) { ui.notifications.error(game.i18n.localize("TWODSIX.Errors.NoROFForAttack")); } const skill:TwodsixItem = this.actor.getOwnedItem(this.data.data.skill) as TwodsixItem; const tmpSettings = {"characteristic": skill?.data.data.characteristic || 'NONE'}; + let usedAmmo = 1; switch (attackType) { case "auto-full": - numberOfAttacks = parseInt(rateOfFire, 10); + numberOfAttacks = rateOfFire; + usedAmmo = 3 * rateOfFire; break; case "auto-burst": - bonusDamage = rateOfFire; + bonusDamage = rateOfFire.toString(); + usedAmmo = parseInt(this.data.data.rateOfFire, 10); break; case "burst-attack-dm": tmpSettings["diceModifier"] = TwodsixItem.burstAttackDM(rateOfFireCE); + usedAmmo = rateOfFireCE; break; case "burst-bonus-damage": bonusDamage = TwodsixItem.burstBonusDamage(rateOfFireCE); + usedAmmo = rateOfFireCE; break; } @@ -63,6 +98,20 @@ export default class TwodsixItem extends Item { return; } + if (this.data.data.useConsumableForAttack) { + const magazine = this.actor.getOwnedItem(this.data.data.useConsumableForAttack) as TwodsixItem; + try { + await magazine.consume(usedAmmo); + } catch(err) { + if (err.name == "NoAmmoError") { + ui.notifications.error(game.i18n.localize("TWODSIX.Errors.NoAmmo")); + return; + } else { + throw err; + } + } + } + for (let i = 0; i < numberOfAttacks; i++) { const roll = await this.skillRoll(false, settings, showInChat); if (game.settings.get("twodsix", "automateDamageRollOnHit") && roll.isSuccess()) { @@ -71,7 +120,6 @@ export default class TwodsixItem extends Item { } } - public async skillRoll(showThrowDialog:boolean, tmpSettings:TwodsixRollSettings = null, showInChat = true):Promise { let skill:TwodsixItem; let item:TwodsixItem; @@ -179,4 +227,25 @@ export default class TwodsixItem extends Item { return '0'; } } + + //////// CONSUMABLE //////// + public async consume(quantity:number):Promise { + const consumableLeft = this.data.data.currentCount - quantity; + if (consumableLeft >= 0) { + await this.update({"data.currentCount": consumableLeft}, {}); + } else { + throw {name: 'NoAmmoError'}; + } + } + + public async refill():Promise { + if (this.data.data.quantity > 1) { + await this.update({ + "data.quantity": this.data.data.quantity - 1, + "data.currentCount": this.data.data.max + }, {}); + } else { + throw {name: 'TooLowQuantityError'}; + } + } } diff --git a/src/module/handlebars.ts b/src/module/handlebars.ts index 5437571d1..3551d6b03 100644 --- a/src/module/handlebars.ts +++ b/src/module/handlebars.ts @@ -36,6 +36,15 @@ export default function registerHandlebarsHelpers():void { } }); + Handlebars.registerHelper('twodsix_localizeConsumable', (type) => { + return game.i18n.localize(`TWODSIX.Items.Consumable.Types.${type}`); + }); + + Handlebars.registerHelper('twodsix_refillText', (subtype, quantity) => { + const refillWord = ["magazine", "power_cell"].includes(subtype) ? "Reload" : "Refill"; + return `${game.i18n.localize(`TWODSIX.Actor.Items.${refillWord}`)} (${quantity - 1})`; + }); + Handlebars.registerHelper('twodsix_skillTotal', (actor, characteristic, value) => { const actorData = actor.data; const characteristicElement = actorData.characteristics[getKeyByValue(TWODSIX.CHARACTERISTICS, characteristic)]; @@ -99,32 +108,31 @@ export default function registerHandlebarsHelpers():void { }); //From https://discord.com/channels/732325252788387980/732328233630171188/790507540818690068 - //Not used yet - // Handlebars.registerHelper("iff", function (v1, operator, v2, options) { - // switch (operator) { - // case '==': - // return (v1 == v2) ? options.fn(this) : options.inverse(this); - // case '===': - // return (v1 === v2) ? options.fn(this) : options.inverse(this); - // case '!=': - // return (v1 != v2) ? options.fn(this) : options.inverse(this); - // case '!==': - // return (v1 !== v2) ? options.fn(this) : options.inverse(this); - // case '<': - // return (v1 < v2) ? options.fn(this) : options.inverse(this); - // case '<=': - // return (v1 <= v2) ? options.fn(this) : options.inverse(this); - // case '>': - // return (v1 > v2) ? options.fn(this) : options.inverse(this); - // case '>=': - // return (v1 >= v2) ? options.fn(this) : options.inverse(this); - // case '&&': - // return (v1 && v2) ? options.fn(this) : options.inverse(this); - // case '||': - // return (v1 || v2) ? options.fn(this) : options.inverse(this); - // default: - // return options.inverse(this); - // } - // }); + Handlebars.registerHelper("iff", function (v1, operator, v2, options) { + switch (operator) { + case '==': + return (v1 == v2) ? options.fn(this) : options.inverse(this); + case '===': + return (v1 === v2) ? options.fn(this) : options.inverse(this); + case '!=': + return (v1 != v2) ? options.fn(this) : options.inverse(this); + case '!==': + return (v1 !== v2) ? options.fn(this) : options.inverse(this); + case '<': + return (v1 < v2) ? options.fn(this) : options.inverse(this); + case '<=': + return (v1 <= v2) ? options.fn(this) : options.inverse(this); + case '>': + return (v1 > v2) ? options.fn(this) : options.inverse(this); + case '>=': + return (v1 >= v2) ? options.fn(this) : options.inverse(this); + case '&&': + return (v1 && v2) ? options.fn(this) : options.inverse(this); + case '||': + return (v1 || v2) ? options.fn(this) : options.inverse(this); + default: + return options.inverse(this); + } + }); } diff --git a/src/module/hooks/deleteItem.ts b/src/module/hooks/deleteItem.ts new file mode 100644 index 000000000..5dac0cde7 --- /dev/null +++ b/src/module/hooks/deleteItem.ts @@ -0,0 +1,12 @@ +import TwodsixActor from "../entities/TwodsixActor"; +import TwodsixItem from "../entities/TwodsixItem"; + +Hooks.on('deleteOwnedItem', async (actor:TwodsixActor, itemData) => { + if (itemData.type === "consumable") { + actor.items.filter((item:TwodsixItem) => item.type !== "skills").forEach(async (item:TwodsixItem) => { + if (item.data.data.consumables.includes(itemData._id) || item.data.data.useConsumableForAttack === itemData._id) { + await item.removeConsumable(itemData._id); + } + }); + } +}); diff --git a/src/module/hooks/updateItem.ts b/src/module/hooks/updateItem.ts new file mode 100644 index 000000000..1203e53d3 --- /dev/null +++ b/src/module/hooks/updateItem.ts @@ -0,0 +1,7 @@ +import TwodsixActor from "../entities/TwodsixActor"; + +Hooks.on('preUpdateOwnedItem', async (actor:TwodsixActor, oldItem: Record, updateData: Record) => { + if (updateData.type && oldItem.type === "weapon" && updateData.type !== "weapon" && oldItem.data.useConsumableForAttack) { + updateData["data.useConsumableForAttack"] = ""; + } +}); diff --git a/src/module/sheets/AbstractTwodsixActorSheet.ts b/src/module/sheets/AbstractTwodsixActorSheet.ts index f33bcb996..4b3ab3630 100644 --- a/src/module/sheets/AbstractTwodsixActorSheet.ts +++ b/src/module/sheets/AbstractTwodsixActorSheet.ts @@ -1,3 +1,5 @@ +import { getDataFromDropEvent, getItemDataFromDropData } from "../utils/sheetUtils"; + export abstract class AbstractTwodsixActorSheet extends ActorSheet { /** @override */ @@ -77,6 +79,23 @@ export abstract class AbstractTwodsixActorSheet extends ActorSheet { } + private updateWithItemSpecificValues(itemData:Record, type:string): void { + switch (type) { + case "skills": + if (!game.settings.get('twodsix', 'hideUntrainedSkills')) { + itemData.data.value = game.system.template.Item.skills.value; + } else { + itemData.data.value = 0; + } + break; + case "weapon": + if (game.settings.get('twodsix', 'hideUntrainedSkills')) { + itemData.data.skill = this.actor.getUntrainedSkill().id; + } + break; + } + } + /** * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset * @param {Event} event The originating click event @@ -101,18 +120,7 @@ export abstract class AbstractTwodsixActorSheet extends ActorSheet { // Remove the type from the dataset since it's in the itemData.type prop. delete itemData.data.type; - - if (itemData.type === 'skills') { - if (!game.settings.get('twodsix', 'hideUntrainedSkills')) { - itemData.data.value = game.system.template.Item.skills.value; - } else { - itemData.data.value = 0; - } - } - - if (game.settings.get('twodsix', 'hideUntrainedSkills') && type === "weapon") { - itemData.data.skill = this.actor.getUntrainedSkill().id; - } + this.updateWithItemSpecificValues(itemData, type); // Finally, create the item! return this.actor.createOwnedItem(itemData); @@ -125,16 +133,7 @@ export abstract class AbstractTwodsixActorSheet extends ActorSheet { protected async _onDrop(event:DragEvent):Promise { event.preventDefault(); - let data:any; - try { - if (!event.dataTransfer) { - return false; - } - data = JSON.parse(event.dataTransfer.getData('text/plain')); - } catch (err) { - console.log(`Twodsix | Drop failed with {err}`); - return false; - } + const data = getDataFromDropEvent(event); if (!data) { console.log(`Twodsix | Dragging something that can't be dragged`); @@ -147,23 +146,7 @@ export abstract class AbstractTwodsixActorSheet extends ActorSheet { } const actor = this.actor; - let itemData; - - if (data.pack) { - // compendium - const pack = game.packs.find((p) => p.collection === data.pack); - if (pack.metadata.entity !== 'Item') { - return; - } - const item = await pack.getEntity(data.id); - itemData = duplicate(item.data); - } else if (data.data) { - // other actor - itemData = duplicate(data.data); - } else { - // items directory - itemData = duplicate(game.items.get(data.id).data); - } + const itemData = await getItemDataFromDropData(data); //If we get here, we're sorting things. @@ -221,6 +204,7 @@ export abstract class AbstractTwodsixActorSheet extends ActorSheet { const tool:Item[] = []; const junk:Item[] = []; const skills:Item[] = []; + const consumable:Item[] = []; // Iterate through items, allocating to containers for (const i of sheetData.items) { @@ -246,6 +230,9 @@ export abstract class AbstractTwodsixActorSheet extends ActorSheet { case 'skills': skills.push(i); break; + case 'consumable': + consumable.push(i); + break; default: break; } @@ -258,6 +245,7 @@ export abstract class AbstractTwodsixActorSheet extends ActorSheet { actorData.augment = augment; actorData.tool = tool; actorData.junk = junk; + actorData.consumable = consumable; actorData.skills = skills; } diff --git a/src/module/sheets/TwodsixActorSheet.ts b/src/module/sheets/TwodsixActorSheet.ts index 372bb6a36..830e378bc 100644 --- a/src/module/sheets/TwodsixActorSheet.ts +++ b/src/module/sheets/TwodsixActorSheet.ts @@ -61,13 +61,16 @@ export class TwodsixActorSheet extends AbstractTwodsixActorSheet { html.find('.rollable').on('click', this._onRollWrapper(this._onSkillRoll)); html.find('.rollable-characteristic').on('click', this._onRollWrapper(this._onRollChar)); - html.find('.roll-damage').on('click', (this._onRollDamage.bind(this))); + html.find('.roll-damage').on('click', this._onRollDamage.bind(this)); - html.find('#joat-skill-input').on('input', (this._updateJoatSkill.bind(this))); - html.find('#joat-skill-input').on('blur', (this._onJoatSkillBlur.bind(this))); + html.find('#joat-skill-input').on('input', this._updateJoatSkill.bind(this)); + html.find('#joat-skill-input').on('blur', this._onJoatSkillBlur.bind(this)); html.find('#joat-skill-input').on('click', (event) => { $(event.currentTarget).trigger("select"); }); + + html.find(".adjust-consumable").on("click", this._onAdjustConsumableCount.bind(this)); + html.find(".refill-button").on("click", this._onRefillConsumable.bind(this)); } @@ -181,4 +184,31 @@ export class TwodsixActorSheet extends AbstractTwodsixActorSheet { private static joatToUndrained(joatValue:number):number { return joatValue + game.system.template.Item.skills.value; } + + private getConsumableItem(event:Event):TwodsixItem { + const itemId = $(event.currentTarget).parents('.consumable-row').data('consumable-id'); + return this.actor.getOwnedItem(itemId) as TwodsixItem; + } + + private async _onAdjustConsumableCount(event:Event): Promise { + const modifier = parseInt(event.currentTarget["dataset"]["value"], 10); + const item = this.getConsumableItem(event); + await item.consume(modifier); + } + + private async _onRefillConsumable(event:Event): Promise { + const item = this.getConsumableItem(event); + try { + await item.refill(); + } catch (err) { + if (err.name === "TooLowQuantityError") { + const refillAction = ["magazine", "power_cell"].includes(item.data.data.subtype) ? "Reload" : "Refill"; + const refillWord = game.i18n.localize(`TWODSIX.Actor.Items.${refillAction}`).toLowerCase(); + const tooFewString = game.i18n.localize("TWODSIX.Errors.TooFewToReload"); + ui.notifications.error(tooFewString.replace("_NAME_", item.name.toLowerCase()).replace("_REFILL_ACTION_", refillWord)); + } else { + throw err; + } + } + } } diff --git a/src/module/sheets/TwodsixItemSheet.ts b/src/module/sheets/TwodsixItemSheet.ts index 17f075686..cc4db01e8 100644 --- a/src/module/sheets/TwodsixItemSheet.ts +++ b/src/module/sheets/TwodsixItemSheet.ts @@ -1,5 +1,7 @@ import {AbstractTwodsixItemSheet} from "./AbstractTwodsixItemSheet"; import {TWODSIX} from "../config"; +import TwodsixItem from "../entities/TwodsixItem"; +import { getDataFromDropEvent, getItemDataFromDropData } from "../utils/sheetUtils"; /** * Extend the basic ItemSheet with some very simple modifications @@ -13,7 +15,8 @@ export class TwodsixItemSheet extends AbstractTwodsixItemSheet { classes: ["twodsix", "sheet", "item"], submitOnClose: true, submitOnChange: true, - tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}] + tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}], + dragDrop: [{dropSelector: null}] }); } @@ -40,7 +43,7 @@ export class TwodsixItemSheet extends AbstractTwodsixItemSheet { DIFFICULTIES: TWODSIX.DIFFICULTIES[game.settings.get('twodsix', 'difficultyListUsed')] }; data.data.config = TWODSIX; - + data.data.isOwned = this.item.isOwned; return data; } @@ -66,6 +69,114 @@ export class TwodsixItemSheet extends AbstractTwodsixItemSheet { return; } + html.find('.consumable-create').on('click', this._onCreateConsumable.bind(this)); + html.find('.consumable-edit').on('click', this._onEditConsumable.bind(this)); + html.find('.consumable-delete').on('click', this._onDeleteConsumable.bind(this)); + html.find('.consumable-use-consumable-for-attack').on('change', this._onChangeUseConsumableForAttack.bind(this)); this.handleContentEditable(html); } + + private getConsumable(event:Event):TwodsixItem { + const li = $(event.currentTarget).parents(".consumable"); + return this.item.actor.getOwnedItem(li.data("consumableId")); + } + + private _onEditConsumable(event:Event):void { + this.getConsumable(event).sheet.render(true); + } + + private async _onDeleteConsumable(event:Event):Promise { + const consumable = this.getConsumable(event); + const bodyTextTemplate = game.i18n.localize("TWODSIX.Items.Consumable.RemoveConsumableFrom"); + const consumableNameString = `"${consumable.name}"`; + const body = bodyTextTemplate.replace("_CONSUMABLE_NAME_", consumableNameString).replace("_ITEM_NAME_", this.item.name); + + await Dialog.confirm({ + title: game.i18n.localize("TWODSIX.Items.Consumable.RemoveConsumable"), + content: `
${body}

`, + yes: async () => await this.item.removeConsumable(consumable._id), + no: () => { + //Nothing + }, + }); + } + + private async _onCreateConsumable():Promise { + if (!this.item.isOwned) { + console.error(`Twodsix | Consumables can only be created for owned items`); + return; + } + const template = 'systems/twodsix/templates/items/dialogs/create-consumable.html'; + const html = await renderTemplate(template, { + consumables: TWODSIX.CONSUMABLES + }); + new Dialog({ + title: `${game.i18n.localize("TWODSIX.Items.Items.New")} ${game.i18n.localize("TWODSIX.itemTypes.consumable")}`, + content: html, + buttons: { + ok: { + label: game.i18n.localize("TWODSIX.Create"), + callback: async (buttonHtml:JQuery) => { + const max = parseInt(buttonHtml.find('.consumable-max').val() as string, 10) || 0; + const data = { + name: buttonHtml.find('.consumable-name').val(), + type: "consumable", + data: { + subtype: buttonHtml.find('.consumable-subtype').val(), + quantity: parseInt(buttonHtml.find('.consumable-quantity').val() as string, 10) || 0, + currentCount: max, + max: max, + } + }; + const newConsumable = await this.item.actor.createOwnedItem(data) as TwodsixItem; + await this.item.addConsumable(newConsumable._id); + } + }, + cancel: { + label: game.i18n.localize("Cancel") + } + }, + default: 'ok', + }).render(true); + } + + private _onChangeUseConsumableForAttack(event:Event):void { + this.item.update({"data.useConsumableForAttack": $(event.currentTarget).val()}); + } + + private static check(cond:boolean, err:string) { + if(cond) { + throw new Error(game.i18n.localize(`TWODSIX.Errors.${err}`)); + } + } + + protected async _onDrop(event:DragEvent):Promise { + event.preventDefault(); + try { + TwodsixItemSheet.check(!this.item.isOwned, "OnlyOwnedItems"); + TwodsixItemSheet.check(this.item.type === "skills", "SkillsConsumables"); + + const data = getDataFromDropEvent(event); + + TwodsixItemSheet.check(!data, "DraggingSomething"); + TwodsixItemSheet.check(data.type !== "Item", "OnlyDropItems"); + + const itemData = await getItemDataFromDropData(data); + + TwodsixItemSheet.check(itemData.type !== "consumable", "OnlyDropConsumables"); + + // If the dropped item has the same actor as the current item let's just use the same id. + let itemId: string; + if (this.item.actor.getOwnedItem(itemData._id)) { + itemId = itemData._id; + } else { + const newItem = await this.item.actor.createEmbeddedEntity("OwnedItem", itemData); + itemId = newItem._id; + } + await this.item.addConsumable(itemId); + } catch (err) { + console.error(`Twodsix | ${err}`); + ui.notifications.error(err); + } + } } diff --git a/src/module/utils/sheetUtils.ts b/src/module/utils/sheetUtils.ts index afb4f3d40..547362bdb 100644 --- a/src/module/utils/sheetUtils.ts +++ b/src/module/utils/sheetUtils.ts @@ -1,5 +1,8 @@ //Assorted utility functions likely to be helpful when displaying characters +import { TwodsixItemData } from "src/types/twodsix"; +import TwodsixItem from "../entities/TwodsixItem"; + // export function pseudoHex(value:number):string { // switch (value) { // case 0: @@ -184,3 +187,33 @@ export function getKeyByValue(object: { [x: string]: unknown; }, value:unknown): //TODO This assumes I always find the value. Bad form really. return Object.keys(object).find(key => object[key] === value); } + +export function getDataFromDropEvent(event:DragEvent):Record { + try { + return JSON.parse(event.dataTransfer.getData('text/plain')); + } catch (err) { + throw new Error(game.i18n.localize("TWODSIX.Errors.DropFailedWith").replace("_ERROR_MSG_", err)); + } +} + +export async function getItemDataFromDropData(data:Record): Promise { + if (data.pack) { + // compendium + const pack = game.packs.find((p) => p.collection === data.pack); + if (pack.metadata.entity !== 'Item') { + throw new Error(game.i18n.localize("TWODSIX.Errors.DraggedCompendiumIsNotItem")); + } + const item = await pack.getEntity(data.id); + return duplicate(item.data); + } else if (data.data) { + // other actor + return duplicate(data.data); + } else { + // items directory + const item = TwodsixItem.collection.get(data.id); + if (!item) { + throw new Error(game.i18n.localize("TWODSIX.Errors.CouldNotFindItem").replace("_ITEM_ID_", data.id)); + } + return duplicate(item.data) as TwodsixItemData; + } +} diff --git a/src/twodsix.ts b/src/twodsix.ts index f7c858a3b..e9fab1b9f 100644 --- a/src/twodsix.ts +++ b/src/twodsix.ts @@ -70,10 +70,10 @@ Hooks.once('init', async function () { registerSettings(); - - const templatePaths = require.context("../static/templates/", true, /\.html$/).keys().map(fileName => { + const templatePaths = require.context("../static/templates", true, /\.html$/).keys().map(fileName => { return "systems/twodsix/templates" + fileName.substring(1); }); + await loadTemplates(templatePaths); // All other hooks are found in the module/hooks directory, and should be in the system.json esModules section. diff --git a/src/types/twodsix.d.ts b/src/types/twodsix.d.ts index 28801c0bd..efeda27bf 100644 --- a/src/types/twodsix.d.ts +++ b/src/types/twodsix.d.ts @@ -1,9 +1,10 @@ //MUST match what's in the template.json (or, at least not contradict it). TODO Should build this from the template.json I guess -export type TwodsixItemType = "equipment" | "weapon" | "armor" | "augment" | "storage" | "tool" | "junk" | "skills"; +export type TwodsixItemType = "equipment" | "weapon" | "armor" | "augment" | "storage" | "tool" | "junk" | "skills" | "consumable"; export interface TwodsixItemData extends ItemData { type:TwodsixItemType; hasOwner:boolean; + _id:string; } export type CharacteristicType = diff --git a/static/lang/en.json b/static/lang/en.json index 17f618ec5..186377d24 100644 --- a/static/lang/en.json +++ b/static/lang/en.json @@ -47,6 +47,8 @@ "AUGMENTS": "AUGMENTS", "Ammo": "Ammo", "Armor": "Armor", + "CONSUMABLES": "Consumables", + "Count": "Count", "CreateItem": "Create Item", "Damage": "Damage", "DeleteItem": "Delete Item", @@ -57,7 +59,10 @@ "Name": "Name", "Qty": "Qty", "Range": "Range", + "Refill": "Refill", + "Reload": "Reload", "ShortDescr": "Short Descr", + "Subtype": "Type", "TL": "TL", "WEAPONS": "WEAPONS", "Weight": "Wt" @@ -99,6 +104,7 @@ "Type": "Roll type" } }, + "Create": "Create", "Handlebars": { "CantShowCharacteristic": "You need to reselect the characteristic for all skills marked with XXX. Sorry..." }, @@ -137,6 +143,7 @@ "AssignJunk": "Assign Junk", "AssignTool": "Assign Tool", "AssignWeapon": "Assign Weapon", + "AssignConsumable": "Assign Consumable", "Inventory": "Inventory", "ItemType": "Type", "MoveStorage": "Move Storage", @@ -159,6 +166,23 @@ "Tool": { "Bonus": "Bonus" }, + "Consumable": { + "Types": { + "air": "Air", + "drugs": "Drugs", + "food": "Food", + "fuel": "Fuel", + "magazine": "Magazine", + "power_cell": "Power Cell", + "other": "Other" + }, + "Size": "Size", + "Subtype": "Type", + "Count": "Count", + "DropConsumablesHere": "You can drop consumables here.", + "RemoveConsumable": "Remove consumable", + "RemoveConsumableFrom": "Remove consumable _CONSUMABLE_NAME_ from _ITEM_NAME_?" + }, "Weapon": { "Ammo": "Ammo", "Damage": "Damage", @@ -168,7 +192,9 @@ "weaponType": "Weapon Type", "damageType": "Damage Type", "rateOfFire": "Rate of Fire/Auto X", - "recoil": "Recoil" + "recoil": "Recoil", + "UseAsAmmunitionForAttack": "Use as ammunition for attack", + "MagazineCost": "Magazine/Power Cell Cost" } }, "Migration": { @@ -308,6 +334,7 @@ "CloseAndCreateNew": "Close and create new", "Copy": "Copy", "Yes": "Yes", + "Name": "Name", "itemTypes": { "equipment": "equipment", "weapon": "weapon", @@ -317,13 +344,24 @@ "tool": "tool", "junk": "junk", "skills": "skills", - "skill": "skill" + "skill": "skill", + "consumable": "consumable" }, "Errors": { "NoSkillForSkillRoll": "You tried to perform a skill roll without specifying a skill", "NoCharacteristicForRoll": "You tried to perform a characteristics roll without specifying a characteristic", "NoROFForAttack": "Rate of fire must be specified for this attack", - "NoDamageForWeapon": "This weapon has not specified the damage it does" + "NoDamageForWeapon": "This weapon has not specified the damage it does", + "TooFewToReload": "Too few _NAME_ to _REFILL_ACTION_", + "NoAmmo": "You are out of ammunition. You need to reload before shooting!", + "OnlyOwnedItems": "Can only drop to items that are owned by an actor", + "SkillsConsumables": "Skills can't have consumables", + "DraggingSomething": "Dragging something that can't be dragged", + "OnlyDropItems": "You can only drop items", + "OnlyDropConsumables": "Only consumables can be dropped on an item.", + "DropFailedWith": "Drop failed with _ERROR_MSG_", + "CouldNotFindItem": "Could not find an item with id: _ITEM_ID_", + "DraggedCompendiumIsNotItem": "The dragged compendium object is not an item." } }, "VeryDifficult": "Very Difficult" diff --git a/static/styles/twodsix.css b/static/styles/twodsix.css index 9ccde0158..020b2a97e 100644 --- a/static/styles/twodsix.css +++ b/static/styles/twodsix.css @@ -610,7 +610,7 @@ a.notes-tab.active { .items-weapons { display: grid; - grid-template-columns: 1em 12em 1.5em 1.5em 3.5em 3em 4em 3em 3em; + grid-template-columns: 1em 14em 1.5em 1.5em 3.5em 3em 5em 3em; gap: 1px 1px; grid-template-areas: '. . . . .'; } @@ -648,6 +648,13 @@ a.notes-tab.active { grid-template-areas: '. . . . .'; } +.items-consumable { + display: grid; + grid-template-columns: 1em 15em 5em 2.5em 6.5em 3em; + gap: 1px 1px; + grid-template-areas: '. . . . .'; +} + /*-------Skill Tab-----------*/ .skill-list { @@ -1599,6 +1606,10 @@ a:hover { margin-top: 0.5em; } +.form-section { + margin-top: 0.5em; +} + .item-range { margin-top: 0.5em; } @@ -1648,6 +1659,66 @@ a:hover { text-shadow: 0 0 10px #00e5ff !important; } +/* ------ Item page - Consumables ------ */ +.consumable-row { + width: 100%; + height: 30px; +} + +.consumable-row .wrapper { + float: right; +} + +.consumable-row .refill-button { + width: 100px; + height: 20px; + border-radius: 10px; + line-height: normal; + cursor: pointer; + margin-top: 0px; + margin-left: 5px; +} + +.consumable-row .consumable-count { + margin-left: 5px; +} + +.consumable-row .refill-button[disabled], .combined-buttons button[disabled] { + cursor: default; +} + +.consumable-row .combined-buttons { + width: 40px; + font-size: 0em; +} + +.consumable-row .combined-buttons button { + width: 20px; + height: 20px; + margin: 0; + padding: 0; + line-height: normal; + cursor: pointer; +} + +.consumable-row .left-button { + border-radius: 10px 0px 0px 10px; +} + +.consumable-row .right-button { + border-left: 0px solid #000; + border-radius: 0px 10px 10px 0px; +} + +.item-sheet .form-consumable-count { + width: 50px; + text-align: center; +} + +.remove-consumable { + text-align: center; +} + /*--- Chat Log ---*/ #sidebar-tabs.tabs .item.active { diff --git a/static/template.json b/static/template.json index 1df7ba524..75e6ee24f 100644 --- a/static/template.json +++ b/static/template.json @@ -275,7 +275,7 @@ } }, "Item": { - "types": ["equipment", "weapon", "armor", "augment", "storage", "tool", "junk", "skills"], + "types": ["equipment", "weapon", "armor", "augment", "storage", "tool", "junk", "skills", "consumable"], "templates": { "gearTemplate": { "name": "", @@ -286,6 +286,7 @@ "weight": 0, "price": "", "traits": [], + "consumables": [], "equipped": { "weight": 0 }, @@ -305,6 +306,7 @@ "damageBonus": 0, "magazineSize": 0, "ammo": 0, + "useConsumableForAttack": "", "magazineCost": 0, "type": "weapon", "location": ["inventory", "storage"], @@ -346,6 +348,19 @@ "key": "key", "difficulty": "Average", "rolltype": "Normal" + }, + "consumable": { + "templates": [ + "gearTemplate" + ], + "currentCount": 0, + "max": 0, + "type": "consumable", + "subtype": "other", + "location": [ + "inventory", + "storage" + ] } } } diff --git a/static/templates/actors/parts/actor/actor-consumable.html b/static/templates/actors/parts/actor/actor-consumable.html new file mode 100644 index 000000000..a35d3c426 --- /dev/null +++ b/static/templates/actors/parts/actor/actor-consumable.html @@ -0,0 +1,17 @@ +
+
+ {{name}}: + {{#with data}} + + {{currentCount}}/{{max}} + + + + + + + {{/with}} +
+
diff --git a/static/templates/actors/parts/actor/actor-items.html b/static/templates/actors/parts/actor/actor-items.html index 3e5763cc8..86b1dd376 100644 --- a/static/templates/actors/parts/actor/actor-items.html +++ b/static/templates/actors/parts/actor/actor-items.html @@ -12,7 +12,6 @@ {{localize "TWODSIX.Actor.Items.Weight"}} {{localize "TWODSIX.Actor.Items.Range"}} {{localize "TWODSIX.Actor.Items.Damage"}} - {{localize "TWODSIX.Actor.Items.Ammo"}} @@ -32,8 +31,7 @@ {{else}} {{data.range}} {{/if}} - {{twodsix_limitLength data.damage 5}} - {{data.ammo}} + {{twodsix_limitLength data.damage 8}} @@ -57,6 +55,9 @@ {{/if}} + {{#each item.data.consumableData as |consumableData|}} + {{> "systems/twodsix/templates/actors/parts/actor/actor-consumable.html" consumableData}} + {{/each}} {{/each}} @@ -94,6 +95,9 @@ + {{#each item.data.consumableData as |consumableData|}} + {{> "systems/twodsix/templates/actors/parts/actor/actor-consumable.html" consumableData}} + {{/each}} {{/each}} @@ -130,6 +134,9 @@ + {{#each item.data.consumableData as |consumableData|}} + {{> "systems/twodsix/templates/actors/parts/actor/actor-consumable.html" consumableData}} + {{/each}} {{/each}} @@ -164,7 +171,53 @@ + {{#each item.data.consumableData as |consumableData|}} + {{> "systems/twodsix/templates/actors/parts/actor/actor-consumable.html" consumableData}} + {{/each}} {{/each}} + + +
+ + {{localize "TWODSIX.Actor.Items.CONSUMABLES"}} +
+ + {{localize "TWODSIX.Actor.Items.Name"}} + {{localize "TWODSIX.Actor.Items.Subtype"}} + {{localize "TWODSIX.Actor.Items.Qty"}} + {{localize "TWODSIX.Actor.Items.Count"}} + + + + + +
+ + {{#each actor.consumable as |item id|}} +
+
    +
  1. + + + d6 + + {{item.name}} + {{twodsix_localizeConsumable data.subtype}} + {{data.quantity}} + {{data.currentCount}}/{{data.max}} + + + + + +
  2. +
+ {{#each item.data.consumableData as |consumableData|}} + {{> "systems/twodsix/templates/actors/parts/actor/actor-consumable.html" consumableData}} + {{/each}} +
+ {{/each}} +
diff --git a/static/templates/items/armor-sheet.html b/static/templates/items/armor-sheet.html index cc5c3ab39..c558b0de0 100644 --- a/static/templates/items/armor-sheet.html +++ b/static/templates/items/armor-sheet.html @@ -43,5 +43,6 @@ {{localize "TWODSIX.Items.Equipment.Description"}}
{{{data.description}}}
+ {{> "systems/twodsix/templates/items/parts/consumables-part.html"}} diff --git a/static/templates/items/augment-sheet.html b/static/templates/items/augment-sheet.html index 792ca477b..4780b64c4 100644 --- a/static/templates/items/augment-sheet.html +++ b/static/templates/items/augment-sheet.html @@ -39,5 +39,6 @@ {{localize "TWODSIX.Items.Equipment.Description"}}
{{{data.description}}}
+ {{> "systems/twodsix/templates/items/parts/consumables-part.html"}} diff --git a/static/templates/items/consumable-sheet.html b/static/templates/items/consumable-sheet.html new file mode 100644 index 000000000..7010f142f --- /dev/null +++ b/static/templates/items/consumable-sheet.html @@ -0,0 +1,50 @@ +
+
+ {{> "systems/twodsix/templates/items/parts/common-parts.html"}} + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ {{localize "TWODSIX.Items.Equipment.Description"}} +
{{{data.description}}}
+
+ {{> "systems/twodsix/templates/items/parts/consumables-part.html"}} +
+
diff --git a/static/templates/items/dialogs/create-consumable.html b/static/templates/items/dialogs/create-consumable.html new file mode 100644 index 000000000..a998c4247 --- /dev/null +++ b/static/templates/items/dialogs/create-consumable.html @@ -0,0 +1,24 @@ +
+ +
+
+ +
+
+ +
+
+ +
diff --git a/static/templates/items/equipment-sheet.html b/static/templates/items/equipment-sheet.html index 8f3f56af9..77f75dcd8 100644 --- a/static/templates/items/equipment-sheet.html +++ b/static/templates/items/equipment-sheet.html @@ -24,5 +24,6 @@ {{localize "TWODSIX.Items.Equipment.Description"}}
{{{data.description}}}
+ {{> "systems/twodsix/templates/items/parts/consumables-part.html"}} diff --git a/static/templates/items/item-sheet.html b/static/templates/items/item-sheet.html index 4fb1e2078..7d0f8091f 100644 --- a/static/templates/items/item-sheet.html +++ b/static/templates/items/item-sheet.html @@ -34,5 +34,6 @@ {{localize "TWODSIX.Items.Equipment.Description"}}
{{{data.description}}}
+ {{> "systems/twodsix/templates/items/parts/consumables-part.html"}} diff --git a/static/templates/items/junk-sheet.html b/static/templates/items/junk-sheet.html index 8f3f56af9..77f75dcd8 100644 --- a/static/templates/items/junk-sheet.html +++ b/static/templates/items/junk-sheet.html @@ -24,5 +24,6 @@ {{localize "TWODSIX.Items.Equipment.Description"}}
{{{data.description}}}
+ {{> "systems/twodsix/templates/items/parts/consumables-part.html"}} diff --git a/static/templates/items/parts/common-parts.html b/static/templates/items/parts/common-parts.html index 6424f5167..d7816c510 100644 --- a/static/templates/items/parts/common-parts.html +++ b/static/templates/items/parts/common-parts.html @@ -25,8 +25,9 @@ - - + + + {{/select}} diff --git a/static/templates/items/parts/consumables-part.html b/static/templates/items/parts/consumables-part.html new file mode 100644 index 000000000..2862df3ae --- /dev/null +++ b/static/templates/items/parts/consumables-part.html @@ -0,0 +1,45 @@ +{{#if data.isOwned}} +
+ {{localize "TWODSIX.Actor.Items.CONSUMABLES"}}:
+ + + + + + + + + + + + {{#each data.consumableData as |consumable|}} + + + + + + + + {{/each}} + {{#unless data.consumableData}} + + + + {{/unless}} + +
{{localize "TWODSIX.Actor.Items.Name"}}{{localize "TWODSIX.Actor.Items.Subtype"}}{{localize "TWODSIX.Actor.Items.Qty"}}{{localize "TWODSIX.Actor.Items.Count"}} + + + +
{{consumable.name}}{{twodsix_localizeConsumable consumable.data.subtype}}{{consumable.data.quantity}}{{consumable.data.currentCount}}/{{consumable.data.max}} + + + + + + +
+ {{localize "TWODSIX.Items.Consumable.DropConsumablesHere"}} +
+
+{{/if}} diff --git a/static/templates/items/storage-sheet.html b/static/templates/items/storage-sheet.html index 8f3f56af9..77f75dcd8 100644 --- a/static/templates/items/storage-sheet.html +++ b/static/templates/items/storage-sheet.html @@ -24,5 +24,6 @@ {{localize "TWODSIX.Items.Equipment.Description"}}
{{{data.description}}}
+ {{> "systems/twodsix/templates/items/parts/consumables-part.html"}} diff --git a/static/templates/items/tool-sheet.html b/static/templates/items/tool-sheet.html index 5500180df..d07a03ffa 100644 --- a/static/templates/items/tool-sheet.html +++ b/static/templates/items/tool-sheet.html @@ -30,5 +30,6 @@ {{localize "TWODSIX.Items.Equipment.Description"}}
{{{data.description}}}
+ {{> "systems/twodsix/templates/items/parts/consumables-part.html"}} diff --git a/static/templates/items/weapon-sheet.html b/static/templates/items/weapon-sheet.html index be2cfc5e9..b1945c7d3 100644 --- a/static/templates/items/weapon-sheet.html +++ b/static/templates/items/weapon-sheet.html @@ -4,40 +4,45 @@ data-actor-id="{{actor._id}}">
{{> "systems/twodsix/templates/items/parts/common-parts.html"}} +
+ +
-
{{#if data.settings.ShowRangeBandAndHideRange}} -
-
-
-
@@ -77,8 +82,8 @@ {{/if}}
-
@@ -86,5 +91,20 @@ {{localize "TWODSIX.Items.Equipment.Description"}}
{{{data.description}}}
+ {{> "systems/twodsix/templates/items/parts/consumables-part.html"}} + {{#if data.isOwned}} +
+ +
+ {{/if}} diff --git a/webpack.config.ts b/webpack.config.ts index 065ab08d8..8487315ea 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -44,7 +44,7 @@ module.exports = (env, argv) => { }, {test: /\.(png|svg|jpg|gif|woff|woff2|eot|ttf|otf)$/, use: ['url-loader?limit=100000']}, - { test: /\.html$/, use: 'file-loader' } + {test: /\.html$/, use: ['file-loader']} ], }, plugins: [