diff --git a/module.json b/module.json index 6a75a12..d31729f 100644 --- a/module.json +++ b/module.json @@ -31,8 +31,8 @@ "id": "item-piles", "type": "module", "compatibility": { - "minimum": "2.8.1", - "verified": "2.8.1" + "minimum": "2.8.6", + "verified": "2.8.6" } }, { diff --git a/src/applications/auctioneer/Auctions/Auctions.svelte b/src/applications/auctioneer/Auctions/Auctions.svelte index 146b5ae..fc33d92 100644 --- a/src/applications/auctioneer/Auctions/Auctions.svelte +++ b/src/applications/auctioneer/Auctions/Auctions.svelte @@ -5,7 +5,6 @@ import SortByTabs from "~/applications/auctioneer/Components/SortByTabs.svelte"; import OwnAuctionItem from "~/applications/auctioneer/Auctions/OwnAuctionItem.svelte"; import CreateAuctionSidebar from "~/applications/auctioneer/Auctions/CreateAuctionSidebar.svelte"; - import { writable } from "svelte/store"; const store = getContext("store"); diff --git a/src/applications/auctioneer/Auctions/CreateAuctionSidebar.svelte b/src/applications/auctioneer/Auctions/CreateAuctionSidebar.svelte index 6953a25..6c25c84 100644 --- a/src/applications/auctioneer/Auctions/CreateAuctionSidebar.svelte +++ b/src/applications/auctioneer/Auctions/CreateAuctionSidebar.svelte @@ -2,11 +2,10 @@ import { getContext } from "svelte"; import { get, writable } from "svelte/store"; - import { TJSDocument } from "#runtime/svelte/store/fvtt/document"; import DropZone from "~/applications/auctioneer/Components/DropZone.svelte"; import CurrencyList from "~/applications/auctioneer/Auctions/CurrencyList.svelte"; import CONSTANTS from "~/constants.js"; - import { getCurrencies } from "~/lib.js"; + import { getCurrencies, getPriceFromData } from "~/lib.js"; import CurrencyStore from "~/applications/auctioneer/Auctions/currency-store.js"; import ReactiveButton from "~/applications/auctioneer/Components/ReactiveButton.svelte"; @@ -15,7 +14,9 @@ const flagStore = store.auctioneerFlags; const flags = get(flagStore); - const itemDocStore = new TJSDocument(); + const itemDocStore = store.auctionItemStore; + const actorDoc = store.actorDoc; + const newAuctionStore = store.newAuctionStore; let showPrice = "bids"; const useSecondaryCurrencies = writable(false); @@ -39,56 +40,26 @@ }); } - const potentialAuctionStore = writable({ - itemData: false, - numAuctions: 1, - quantityPerAuction: 1, - hiddenQuantity: false, - priceIsPerQuantity: false - }); + let baseDepositPrice; + let depositCurrenciesString; + let depositCurrencies; + $: { + baseDepositPrice = $newAuctionStore.depositPrice && $newAuctionStore.quantityPerAuction + ? game.itempiles.API.calculateCurrencies(getPriceFromData($newAuctionStore.depositPrice)?.basePriceString, $newAuctionStore.quantityPerAuction, false) ?? false + : false; + depositCurrenciesString = baseDepositPrice + ? game.itempiles.API.calculateCurrencies(baseDepositPrice, $newAuctionStore.numAuctions, false) ?? false + : false; + depositCurrencies = depositCurrenciesString + ? getPriceFromData(depositCurrenciesString, $actorDoc) + : false; + } const durationStore = writable(get(flagStore).minTimeLimit); - async function dropItem(dropData) { - - const item = await Item.implementation.fromDropData(dropData); - - if (!item.parent?.isOwner && !game.user.isGM) return; - - if (currencies.some(currency => { - return currency.data.item - && currency.data.item.name === item.name - && currency.data.item.type === item.type - && currency.data.item.img === item.img - })) { - return; - } - - if (game.itempiles.API.isItemInvalid(item)) { - return; - } - - const canItemStack = game.itempiles.API.canItemStack(item); - const itemQuantity = game.itempiles.API.getItemQuantity(item); - - potentialAuctionStore.update(data => { - data.numAuctions = 1; - data.quantityPerAuction = itemQuantity; - data.hiddenQuantity = !canItemStack; - data.itemData = item.toObject(); - data.itemCost = game.itempiles.API.getCostOfItem(item); - data.uuid = dropData.uuid; - return data; - }); - - itemDocStore.set(item); - - } - function setMaxAuctions() { - const itemQuantity = game.itempiles.API.getItemQuantity($itemDocStore); - potentialAuctionStore.update(data => { - data.numAuctions = itemQuantity; + newAuctionStore.update(data => { + data.numAuctions = game.itempiles.API.getItemQuantity($itemDocStore); data.quantityPerAuction = 1; return data; }); @@ -96,7 +67,7 @@ function setMaxPerAuction() { const itemQuantity = game.itempiles.API.getItemQuantity($itemDocStore); - potentialAuctionStore.update(data => { + newAuctionStore.update(data => { data.quantityPerAuction = Math.max(1, Math.floor(itemQuantity / data.numAuctions)); return data; }); @@ -104,7 +75,7 @@ function numAuctionsChanged() { const itemQuantity = game.itempiles.API.getItemQuantity($itemDocStore); - potentialAuctionStore.update(data => { + newAuctionStore.update(data => { data.numAuctions = Math.min(data.numAuctions, itemQuantity); data.quantityPerAuction = Math.min(data.quantityPerAuction, Math.floor(itemQuantity / data.numAuctions)); return data; @@ -113,19 +84,22 @@ function numQuantityPerAuctionChanged() { const itemQuantity = game.itempiles.API.getItemQuantity($itemDocStore); - potentialAuctionStore.update(data => { + newAuctionStore.update(data => { data.quantityPerAuction = Math.min(data.quantityPerAuction, itemQuantity); data.numAuctions = Math.min(data.numAuctions, Math.floor(itemQuantity / data.quantityPerAuction)); return data; }); } - async function postAuctions() { - const result = await store.postAuctions({ - numAuctions: $potentialAuctionStore.numAuctions, - quantityPerAuction: $potentialAuctionStore.quantityPerAuction, - itemData: $potentialAuctionStore.itemData, - uuid: $potentialAuctionStore.uuid, + async function createAuctions(withoutDeposit = false) { + const result = await store.createAuctions({ + numAuctions: $newAuctionStore.numAuctions, + quantityPerAuction: $newAuctionStore.quantityPerAuction, + itemData: $newAuctionStore.itemData, + uuid: $newAuctionStore.uuid, + priceIsPerQuantity: $newAuctionStore.priceIsPerQuantity, + baseDepositPrice: withoutDeposit ? false : baseDepositPrice, + depositPrice: withoutDeposit ? false : depositCurrenciesString, bidVisibility: $bidVisibility, reserveLimitVisibility: $reserveLimitVisibility, bidCurrencies: bidCurrencies.exportCurrencies(), @@ -134,13 +108,16 @@ reserveCurrencies: reserveCurrencies.exportCurrencies(), duration: $durationStore }); - if(!result) return; - potentialAuctionStore.set({ + if (!result) return; + itemDocStore.set(undefined); + newAuctionStore.set({ itemData: false, numAuctions: 1, quantityPerAuction: 1, hiddenQuantity: false, - priceIsPerQuantity: false + priceIsPerQuantity: false, + uuid: false, + depositPrice: false, }); bidCurrencies.reset(); buyoutCurrencies.reset(); @@ -156,33 +133,57 @@
Auction Item - + {$itemDocStore ? $itemDocStore.name : "Drag & drop item"} -
-
Num. Auctions
- - - {#if !$potentialAuctionStore.hiddenQuantity} + {#if !$newAuctionStore.hiddenQuantity} +
+
Num. Auctions
+ + Qty/Auctions - - - {/if} -
-
- Price is per quantity: - -
+ +
+
+ Price is per quantity: + +
+ {:else} +
+
Item cannot be stacked, only 1 auction can be created at a time.
+
+ {/if} {#if $flagStore.allowSecondaryCurrencies && hasSecondaryCurrencies} -
+
Use Other Currencies
{/if} + {#if $flagStore.auctionDeposit} +
+ Auction deposit: + {depositCurrenciesString || "None"} +
+ {/if} {#if $flagStore.auctionBidVisibility === CONSTANTS.VISIBILITY_KEYS.USER} -
Bids Visibility
+
+ Bids Visibility +
{/if} {#if $flagStore.auctionBidVisibility === CONSTANTS.VISIBILITY_KEYS.USER}
@@ -198,22 +199,25 @@
{:else if $flagStore.auctionBidVisibility === CONSTANTS.VISIBILITY_KEYS.VISIBLE} -
+
Visible Bids
{:else if $flagStore.auctionBidVisibility === CONSTANTS.VISIBILITY_KEYS.HIDDEN} -
+
Hidden Bids
{/if}
+ currencyStore={bidCurrencies} label="Starting Bid" name="bids" + tooltip="Starting price for bids" {useSecondaryCurrencies}/> + currencyStore={buyoutCurrencies} label="Buyout Price" + name="bids" + tooltip="Price to pay to instantly win this auction" {useSecondaryCurrencies}/>
{#if $flagStore.enableMinimumBid || $flagStore.enableReserveLimit}
@@ -231,7 +235,9 @@ {/if} {#if $flagStore.enableReserveLimit} {#if $flagStore.reserveLimitVisibility === CONSTANTS.VISIBILITY_KEYS.USER} -
Reserve Visibility
+
Reserve Visibility +
{/if} {#if $flagStore.reserveLimitVisibility === CONSTANTS.VISIBILITY_KEYS.USER}
@@ -247,16 +253,19 @@
{:else if $flagStore.reserveLimitVisibility === CONSTANTS.VISIBILITY_KEYS.VISIBLE} -
+
Visible Reserve
{:else if $flagStore.reserveLimitVisibility === CONSTANTS.VISIBILITY_KEYS.HIDDEN} -
+
Hidden Reserve
{/if} {/if} -
+
Duration: Ignore deposit payment

+ `, + yes: (html) => { + ignoreDeposit = html.find("input").is(":checked") + }, + options: { classes: ["dialog", "item-piles-auctioneer"] } + }); + if (!proceed) return; + if(!ignoreDeposit) { + await game.itempiles.API.removeCurrencies(actor, existingAuctions[indexToRefresh].depositPrice); + } + }else { + const proceed = await Dialog.confirm({ + title: "Relist Failed Auction", + content: `

By relisting this auction, ${actor.name} must pay ${existingAuctions[indexToRefresh].depositPrice} - are you sure you want to do this?

`, + options: { classes: ["dialog", "item-piles-auctioneer"] } + }); + if (!proceed) return; + await game.itempiles.API.removeCurrencies(actor, existingAuctions[indexToRefresh].depositPrice); + } + } + } ui.notifications.notify(`The auction for ${auction.item.name} has been relisted.`); return game.user.setFlag(CONSTANTS.MODULE_NAME, CONSTANTS.AUCTIONS_FLAG, existingAuctions); } @@ -570,6 +755,11 @@ export default function (auctioneer) { update, subscribe, unsubscribe, + + auctionItemStore, + onDropData, + newAuctionStore, + searchClicked, decrementPage, incrementPage, @@ -578,7 +768,7 @@ export default function (auctioneer) { bidOnItem, buyoutItem, - postAuctions, + createAuctions, claimAuctions, relistAuction, @@ -607,7 +797,7 @@ export default function (auctioneer) { * @param {Actor} auctioneer * @return {AuctionData} */ -function getAuctioneerActorData(auctioneer) { +export function getAuctioneerActorData(auctioneer) { const auctions = {}; const bids = {}; @@ -618,9 +808,9 @@ function getAuctioneerActorData(auctioneer) { const userBids = lib.getUserBids(user); const userBuyouts = lib.getUserBuyouts(user); return { - auctionFlags: acc.auctionFlags.concat(userAuctions.map(source => ({ ...source, userUuid: user.uuid }))), - bidFlags: acc.bidFlags.concat(userBids.map(source => ({ ...source, userUuid: user.uuid }))), - buyoutFlags: acc.buyoutFlags.concat(userBuyouts.map(source => ({ ...source, userUuid: user.uuid }))), + auctionFlags: acc.auctionFlags.concat(userAuctions), + bidFlags: acc.bidFlags.concat(userBids), + buyoutFlags: acc.buyoutFlags.concat(userBuyouts), } }, { auctionFlags: [], bidFlags: [], buyoutFlags: [] }); @@ -629,59 +819,16 @@ function getAuctioneerActorData(auctioneer) { buyoutFlags.sort((a, b) => b.date - a.date); auctionFlags.filter(source => { - return source.uuid.endsWith(auctioneer.uuid + "-" + source.userUuid); + return source.uuid.endsWith(auctioneer.uuid + "-" + source.userId); }).forEach(source => { - const auction = {}; - auction._source = foundry.utils.mergeObject(foundry.utils.deepClone(CONSTANTS.DEFAULTS.AUCTION), source); - auction.type = "auction"; - auction.id = auction._source.id; - auction.uuid = auction._source.uuid; - auction.cancelled = auction._source.cancelled; - auction.claimed = auction._source.claimed; - auction.item = new Item.implementation(auction._source.itemData) - auction.user = fromUuidSync(auction._source.userUuid); - auction.actor = auction._source.actorUuid ? fromUuidSync(auction._source.actorUuid) : false; - auction.date = auction._source.date; - auction.claimedDate = auction._source.claimedDate; - auction.expiryDate = auction._source.expiryDate; - auction.expired = lib.evaluateFoundryTime(auctioneer) >= auction._source.expiryDate; - auction.timeLeft = lib.dateNumberToRelativeString(auctioneer, auction._source.expiryDate); - auction.quantity = auction._source.quantity; - auction.bidVisibility = auction._source.bidVisibility; - auction.startPrice = auction._source.startPrice; - auction.buyoutPrice = auction._source.buyoutPrice; - auction.reservePrice = auction._source.reservePrice; - auction.minBidPrice = auction._source.minBidPrice; - - auction.startPriceData = lib.getPriceFromData(auction._source.startPrice); - auction.buyoutPriceData = lib.getPriceFromData(auction._source.buyoutPrice); - auction.reservePriceData = lib.getPriceFromData(auction._source.reservePrice); - auction.minBidPriceData = lib.getPriceFromData(auction._source.minBidPrice); - - auction.won = false; - auction.bids = []; - auction.bidPriceData = false; - auction.highestOwnedBid = false; - + const auction = lib.makeAuction(auctioneer, source); auctions[auction.uuid] = auction; }); buyoutFlags.filter(source => { return source.auctionUuid.includes("-" + auctioneer.uuid + "-") && auctions[source.auctionUuid]; }).forEach(source => { - const buyout = {}; - buyout._source = foundry.utils.mergeObject(foundry.utils.deepClone(CONSTANTS.DEFAULTS.BUYOUT), source); - buyout.type = "buyout"; - buyout.id = buyout._source.id; - buyout.date = buyout._source.date; - buyout.user = fromUuidSync(buyout._source.userUuid); - buyout.actor = buyout._source.actorUuid ? fromUuidSync(buyout._source.actorUuid) : false; - buyout.priceData = lib.getPriceFromData(buyout._source.price); - buyout.price = buyout._source.price; - buyout.claimed = buyout._source.claimed; - buyout.auction = auctions[buyout._source.auctionUuid]; - buyout.auction.won = buyout; - + const buyout = lib.makeBuyout(auctioneer, source, auctions); buyouts[buyout.id] = buyout; }); @@ -690,23 +837,8 @@ function getAuctioneerActorData(auctioneer) { && auctions[source.auctionUuid] && !auctions[source.auctionUuid].won; }).forEach(source => { - const bid = {}; - bid._source = foundry.utils.mergeObject(foundry.utils.deepClone(CONSTANTS.DEFAULTS.BID), source); - - bid.type = "bid"; - bid.id = bid._source.id; - bid.date = bid._source.date; - bid.user = fromUuidSync(bid._source.userUuid); - bid.actor = bid._source.actorUuid ? fromUuidSync(bid._source.actorUuid) : false; - bid.priceData = lib.getPriceFromData(bid._source.price); - bid.price = bid._source.price; - bid.claimed = bid._source.claimed; - bid.bidStatus = { value: -Infinity, label: "Low Bid" }; - bid.auction = auctions[bid._source.auctionUuid]; - bid.auction.bids.push(bid); - + const bid = lib.makeBid(auctioneer, source, auctions); bids[bid.id] = bid; - }); for (const auction of Object.values(auctions)) { @@ -734,15 +866,24 @@ function getAuctioneerActorData(auctioneer) { } } - auction.highBidder = auction.bids?.[0]?.user; - - if (auction.expired && auction.bids.length) { - if(!auction.reservePrice || !lib.isPriceHigherThan(auction.bids[0].priceData, auction.reservePriceData)){ - auction.won = auction.bids[0]; - if(auction.user === game.user) { - auction.timeLeft = { - label: "Auction Succeeded", - value: Infinity + if (auction.won) { + if (auction.user === game.user) { + auction.timeLeft = { + label: "Auction Succeeded", + value: Infinity + } + } + auction.highBidder = auction.won.user.name + " (buyout)"; + } else { + auction.highBidder = auction.bids?.[0]?.user?.name; + if (auction.expired && auction.bids.length) { + if (!auction.reservePrice || !lib.isPriceHigherThan(auction.bids[0].priceData, auction.reservePriceData)) { + auction.won = auction.bids[0]; + if (auction.user === game.user) { + auction.timeLeft = { + label: "Auction Succeeded", + value: Infinity + } } } } diff --git a/src/applications/auctioneer/auctioneer.js b/src/applications/auctioneer/auctioneer.js index 184a1e2..e7e7835 100644 --- a/src/applications/auctioneer/auctioneer.js +++ b/src/applications/auctioneer/auctioneer.js @@ -16,6 +16,10 @@ export default class Auctioneer extends SvelteApplication { this.actor = options.auctioneer; } + onDropData(data){ + return this.store.onDropData(data); + } + /** @inheritdoc */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { @@ -32,6 +36,10 @@ export default class Auctioneer extends SvelteApplication { }); } + get store() { + return this.svelte.applicationShell.store; + } + static async show(options = {}, dialogData = {}) { const app = lib.getActiveApps(`${Auctioneer.ID}-${options.auctioneer.id}`, true); if (app) { diff --git a/src/constants.js b/src/constants.js index 7ca2fed..cb7e695 100644 --- a/src/constants.js +++ b/src/constants.js @@ -14,6 +14,12 @@ const CONSTANTS = { AUCTIONS_FLAG: "auctions", BIDS_FLAG: "bids", BUYOUTS_FLAG: "buyouts", + LOGS_FLAG: "logs", + + AUCTIONS_FULL_FLAG: `flags.${module_name}.auctions`, + BIDS_FULL_FLAG: `flags.${module_name}.bids`, + BUYOUTS_FULL_FLAG: `flags.${module_name}.buyouts`, + LOGS_FULL_FLAG: `flags.${module_name}.logs`, AUCTIONEER: "auctioneer", @@ -22,25 +28,32 @@ const CONSTANTS = { /** * @typedef {Object} ActorFlagDefaults * @property {number} auctionFee + * @property {string} auctionDeposit * @property {boolean} allowSecondaryCurrencies * @property {boolean} enableMinimumBid * @property {boolean} enableReserveLimit + * @property {boolean} visibleTimeLeft * @property {string} auctionBidVisibility * @property {string} reserveLimitVisibility * @property {AuctionTimeType} timeType * @property {string} minTimeLimit * @property {string} maxTimeLimit + * @property {boolean} displayEntryItem */ ACTOR_DEFAULTS: { auctionFee: 5, + auctionDeposit: "@itemCost * 0.05", allowSecondaryCurrencies: true, enableMinimumBid: false, enableReserveLimit: false, + visibleTimeLeft: false, + allowBankerVaults: true, auctionBidVisibility: "user", reserveLimitVisibility: "visible", timeType: "realTime", minTimeLimit: "12hours", maxTimeLimit: "2days", + displayEntryItem: false, }, AUCTION_TIME_LIMITS: { @@ -62,11 +75,16 @@ const CONSTANTS = { DEFAULTS: { /** * @typedef {Object} Auction + * @property {String} id + * @property {String} userId * @property {String} uuid - * @property {String} ownerActorUuid + * @property {String} actorUuid * @property {Object} item - * @property {Number} startPrice - * @property {Number} buyoutPrice + * @property {String} startPrice + * @property {String} buyoutPrice + * @property {String} minBidPrice + * @property {String} reservePrice + * @property {String} depositPrice * @property {Number} quantity * @property {String} bidVisibility * @property {Number} date @@ -74,6 +92,7 @@ const CONSTANTS = { */ AUCTION: { id: "", + userId: "", uuid: "AUCTIONID-AUCTIONEERUUID-USERUUID", actorUuid: "", itemData: {}, @@ -81,10 +100,12 @@ const CONSTANTS = { buyoutPrice: "", minBidPrice: "", reservePrice: "", + depositPrice: "", quantity: 1, bidVisibility: "visible", reserveLimitVisibility: "visible", claimed: false, + gmClaimed: false, cancelled: false, date: 0, expiryDate: 0, @@ -93,12 +114,14 @@ const CONSTANTS = { /** * @typedef {Object} Bid * @property {String} id + * @property {String} userId * @property {String} auctionUuid * @property {Number} price * @property {Number} date */ BID: { id: "", + userId: "", auctionUuid: "", actorUuid: "", price: "", @@ -109,12 +132,14 @@ const CONSTANTS = { /** * @typedef {Object} Buyout * @property {String} id + * @property {String} userId * @property {String} auctionUuid * @property {Number} price * @property {Number} date */ BUYOUT: { id: "", + userId: "", auctionUuid: "", actorUuid: "", price: "", @@ -168,11 +193,17 @@ CONSTANTS.RESERVE_VISIBILITY_LABELS = { CONSTANTS.AUCTIONEER_SETTINGS = { auctionFee: { - title: "Auction Fee", - label: "This is the percentage cut the auctioneer takes of any successful auction.", + title: "Auction Fee Formula", + label: "This is the percentage of the total sell price that the auctioneer takes as a cut from any successful auction.", type: Number, value: CONSTANTS.ACTOR_DEFAULTS.auctionFee }, + auctionDeposit: { + title: "Deposit Fee Formula", + label: "This is the formula to calculate the price that someone must pay to put up an auction.", + type: String, + value: CONSTANTS.ACTOR_DEFAULTS.auctionDeposit + }, auctionBidVisibility: { title: "Bid Visibility", label: "This configures whether the user has control over the visibility of bids on their auctions, or whether it is forced to be visible or blind.", @@ -186,6 +217,12 @@ CONSTANTS.AUCTIONEER_SETTINGS = { type: Boolean, value: CONSTANTS.ACTOR_DEFAULTS.allowSecondaryCurrencies }, + visibleTimeLeft: { + title: "Show full time left", + label: "When enabled, instead of 'very long' or 'short' strings for auction duration, this will show an exact amount of time left before auctions expire.", + type: Boolean, + value: CONSTANTS.ACTOR_DEFAULTS.visibleTimeLeft + }, enableMinimumBid: { title: "Enable Minimum Bid Limit", label: "When enabled, players can set a minimum bid amount on an auction that each bid must have to be placed on it.", @@ -226,12 +263,24 @@ CONSTANTS.AUCTIONEER_SETTINGS = { options: CONSTANTS.AUCTION_TIME_LIMITS, value: CONSTANTS.ACTOR_DEFAULTS.maxTimeLimit }, + allowBankerVaults: { + title: "Allow banker vault impersonation", + label: "When enabled, this allows users to impersonate the vaults connected to bankers when interacting with the auctioneer. They can buy and sell items with it.", + type: Boolean, + value: CONSTANTS.ACTOR_DEFAULTS.allowBankerVaults + }, entryItem: { title: "Entry Item", - label: "Doesn't work yet.", + label: "This configures an item that the character must possess in their inventory in order to access the auction house.", type: Item, - value: {} - } + value: false + }, + displayEntryItem: { + title: "Display Entry Item", + label: "This determines whether the auctioneer accepts secondary currencies for its auctions", + type: Boolean, + value: CONSTANTS.ACTOR_DEFAULTS.displayEntryItem + }, }; export default CONSTANTS; diff --git a/src/lib.js b/src/lib.js index 83390d0..e3cdb9c 100644 --- a/src/lib.js +++ b/src/lib.js @@ -4,90 +4,99 @@ import moment from "moment"; export function auctioneerRendered(itemPile) { - const auctioneer = itemPile?.actor ?? itemPile; + const auctioneer = itemPile?.actor ?? itemPile; - const flags = auctioneer.getFlag("item-piles", 'data'); + const flags = auctioneer.getFlag("item-piles", 'data'); - if (flags?.type !== CONSTANTS.AUCTIONEER) return; + if (flags?.type !== CONSTANTS.AUCTIONEER) return; - Auctioneer.show({ auctioneer }); + Auctioneer.show({ auctioneer }); - return false; + return false; } export function getActiveApps(id, single = false) { - const apps = Object.values(ui.windows).filter(app => { - return app.id.startsWith(id) && app._state > Application.RENDER_STATES.CLOSED; - }); - if (single) { - return apps?.[0] ?? false; - } - return apps; + const apps = Object.values(ui.windows).filter(app => { + return app.id.startsWith(id) && app._state > Application.RENDER_STATES.CLOSED; + }); + if (single) { + return apps?.[0] ?? false; + } + return apps; } export function abbreviateNumbers(number, decPlaces = 2) { - // 2 decimal places => 100, 3 => 1000, etc - decPlaces = Math.pow(10, decPlaces) + // 2 decimal places => 100, 3 => 1000, etc + decPlaces = Math.pow(10, decPlaces) - // Enumerate number abbreviations - let abbrev = ['k', 'm', 'b', 't'] + // Enumerate number abbreviations + let abbrev = ['k', 'm', 'b', 't'] - // Go through the array backwards, so we do the largest first - for (let i = abbrev.length - 1; i >= 0; i--) { + // Go through the array backwards, so we do the largest first + for (let i = abbrev.length - 1; i >= 0; i--) { - // Convert array index to "1000", "1000000", etc - let size = Math.pow(10, (i + 1) * 3) + // Convert array index to "1000", "1000000", etc + let size = Math.pow(10, (i + 1) * 3) - // If the number is bigger or equal do the abbreviation - if (size <= number) { - // Here, we multiply by decPlaces, round, and then divide by decPlaces. - // This gives us nice rounding to a particular decimal place. - number = Math.round((number * decPlaces) / size) / decPlaces + // If the number is bigger or equal do the abbreviation + if (size <= number) { + // Here, we multiply by decPlaces, round, and then divide by decPlaces. + // This gives us nice rounding to a particular decimal place. + number = Math.round((number * decPlaces) / size) / decPlaces - // Handle special case where we round up to the next abbreviation - if (number === 1000 && i < abbrev.length - 1) { - number = 1 - i++ - } + // Handle special case where we round up to the next abbreviation + if (number === 1000 && i < abbrev.length - 1) { + number = 1 + i++ + } - // Add the letter for the abbreviation - number += abbrev[i] + // Add the letter for the abbreviation + number += abbrev[i] - // We are done... stop - break; - } - } + // We are done... stop + break; + } + } - return number + return number } const relativeDateStrings = [ - [0, "Auction Failed"], - [0.5, "Short"], - [2, "Medium"], - [12, "Long"], - [24, "Very Long"], + [0, "Auction Failed"], + [0.5, "Short"], + [2, "Medium"], + [12, "Long"], + [24, "Very Long"], ] export function dateNumberToRelativeString(auctioneer, date) { + const flags = getAuctioneerActorFlags(auctioneer); - const now = evaluateFoundryTime(auctioneer); + const now = evaluateFoundryTime(auctioneer); - const hours = (date - now) / 1000 / 60 / 60; + if (flags.visibleTimeLeft) { + const value = (date - now); + return { + value: value, + label: value > 0 ? dateTimeToString(auctioneer, date, true) : "Auction Expired" + } + } - for (const [value, label] of relativeDateStrings) { - if (hours <= value) return { value, label }; - } + const hours = (date - now) / 1000 / 60 / 60; - return { - value: hours, - label: Math.floor(hours / 24) + " day" + (Math.floor(hours / 24) > 1 ? "s" : "") - }; + for (const [value, label] of relativeDateStrings) { + if (hours <= value) return { value, label }; + } + + return { + value: hours, + label: Math.floor(hours / 24) + " day" + (Math.floor(hours / 24) > 1 ? "s" : "") + }; } @@ -98,66 +107,66 @@ export function dateNumberToRelativeString(auctioneer, date) { */ export function getPriceFromData(priceFlag, actor = false) { - if (!priceFlag) { - return { - valid: false, - canBuy: false, - currencies: [], - totalPrice: 0 - }; - } - - const paymentData = game.itempiles.API.getPaymentData(priceFlag, { target: actor }); - - const currencies = paymentData.finalPrices.reverse(); - - let primaryCurrency; - for(const currency of currencies){ - if(!currency.exchangeRate || currency.exchangeRate < 1) continue; - if(currency.exchangeRate === 1){ - primaryCurrency = currency; - }else if(primaryCurrency && primaryCurrency.quantity >= (currency.exchangeRate * 1000)){ - currency.quantity = Math.floor(primaryCurrency.quantity / currency.exchangeRate) - primaryCurrency.quantity -= Math.floor(primaryCurrency.quantity / currency.exchangeRate) * currency.exchangeRate; - } - } - - return { - ...paymentData, - valid: true, - currencies: currencies.reverse().filter(currency => currency.quantity), - totalPrice: paymentData.totalCurrencyCost + paymentData.finalPrices - .filter(currency => currency.secondary && currency.quantity) - .reduce((acc, currency) => { - return acc + currency.quantity; - }, 0) - }; + if (!priceFlag) { + return { + valid: false, + canBuy: false, + currencies: [], + totalPrice: 0 + }; + } + + const paymentData = game.itempiles.API.getPaymentData(priceFlag, { target: actor }); + + const currencies = paymentData.finalPrices.reverse(); + + let primaryCurrency; + for (const currency of currencies) { + if (!currency.exchangeRate || currency.exchangeRate < 1) continue; + if (currency.exchangeRate === 1) { + primaryCurrency = currency; + } else if (primaryCurrency && primaryCurrency.quantity >= (currency.exchangeRate * 1000)) { + currency.quantity = Math.floor(primaryCurrency.quantity / currency.exchangeRate) + primaryCurrency.quantity -= Math.floor(primaryCurrency.quantity / currency.exchangeRate) * currency.exchangeRate; + } + } + + return { + ...paymentData, + valid: true, + currencies: currencies.reverse().filter(currency => currency.quantity), + totalPrice: paymentData.totalCurrencyCost + paymentData.finalPrices + .filter(currency => currency.secondary && currency.quantity) + .reduce((acc, currency) => { + return acc + currency.quantity; + }, 0) + }; } export function getValidCurrenciesForPrice(currencies) { - const defaultIncomingCurrencies = currencies.filter(currency => !currency.secondary); + const defaultIncomingCurrencies = currencies.filter(currency => !currency.secondary); - const defaultCurrencies = defaultIncomingCurrencies.length > 0 - ? game.itempiles.API.CURRENCIES.map(currency => { - currency.quantity = 0; - currency.secondary = false; - return currency; - }) : []; + const defaultCurrencies = defaultIncomingCurrencies.length > 0 + ? game.itempiles.API.CURRENCIES.map(currency => { + currency.quantity = 0; + currency.secondary = false; + return currency; + }) : []; - const secondaryIncomingCurrencies = currencies.filter(currency => currency.secondary); - const secondaryCurrencies = game.itempiles.API.SECONDARY_CURRENCIES - .filter(currency => secondaryIncomingCurrencies.some(inCurrency => - (currency.data.uuid && inCurrency.data.uuid && currency.data.uuid === inCurrency.data.uuid) - || (currency.data.path && inCurrency.data.path && currency.data.path === inCurrency.data.path)) - ).map(currency => { - currency.quantity = 0; - currency.secondary = true; - return currency; - }); + const secondaryIncomingCurrencies = currencies.filter(currency => currency.secondary); + const secondaryCurrencies = game.itempiles.API.SECONDARY_CURRENCIES + .filter(currency => secondaryIncomingCurrencies.some(inCurrency => + (currency.data.uuid && inCurrency.data.uuid && currency.data.uuid === inCurrency.data.uuid) + || (currency.data.path && inCurrency.data.path && currency.data.path === inCurrency.data.path)) + ).map(currency => { + currency.quantity = 0; + currency.secondary = true; + return currency; + }); - return defaultCurrencies.concat(secondaryCurrencies) + return defaultCurrencies.concat(secondaryCurrencies) } @@ -166,126 +175,410 @@ export function getValidCurrenciesForPrice(currencies) { * @returns {ActorFlagDefaults} */ export function getAuctioneerActorFlags(actor) { - const defaults = foundry.utils.deepClone(CONSTANTS.ACTOR_DEFAULTS); - return foundry.utils.mergeObject(defaults, actor ? actor.getFlag(CONSTANTS.ITEM_PILES_MODULE, "data") : {}); + const defaults = foundry.utils.deepClone(CONSTANTS.ACTOR_DEFAULTS); + const actorFlags = foundry.utils.deepClone(actor ? actor.getFlag(CONSTANTS.ITEM_PILES_MODULE, "data") : {}); + return foundry.utils.mergeObject(defaults, actorFlags); } export function getCurrencies(actor) { - const flags = getAuctioneerActorFlags(actor); + const flags = getAuctioneerActorFlags(actor); - const defaultCurrencies = foundry.utils.deepClone(game.itempiles.API.CURRENCIES).map(currency => { - currency.quantity = 0; - currency.secondary = false; - return currency; - }); + const defaultCurrencies = foundry.utils.deepClone(game.itempiles.API.CURRENCIES).map(currency => { + currency.quantity = 0; + currency.secondary = false; + return currency; + }); - const secondaryCurrencies = flags.allowSecondaryCurrencies - ? foundry.utils.deepClone(game.itempiles.API.SECONDARY_CURRENCIES).map(currency => { - currency.quantity = 0; - currency.secondary = true; - return currency; - }) - : []; + const secondaryCurrencies = flags.allowSecondaryCurrencies + ? foundry.utils.deepClone(game.itempiles.API.SECONDARY_CURRENCIES).map(currency => { + currency.quantity = 0; + currency.secondary = true; + return currency; + }) + : []; - return defaultCurrencies.concat(secondaryCurrencies); + return defaultCurrencies.concat(secondaryCurrencies); } export function turnCurrenciesIntoString(currencies, abbreviate = false) { - return currencies.filter(currencies => currencies.quantity) - .reduce((acc, currency) => { - const quantity = abbreviate ? abbreviateNumbers(currency.quantity) + " " : currency.quantity; - return `${acc} ${currency.abbreviation.replace('{#}', quantity)}`; - }, "").trim(); + return currencies.filter(currencies => currencies.quantity) + .reduce((acc, currency) => { + const quantity = abbreviate ? abbreviateNumbers(currency.quantity) + " " : currency.quantity; + return `${acc} ${currency.abbreviation.replace('{#}', quantity)}`; + }, "").trim(); } -export function isPriceHigherThan(priceDataA, priceDataB){ - if(priceDataA.primary){ - return priceDataA.totalPrice >= priceDataB.totalPrice; - } - const mixedPrices = priceDataB.currencies.map(currencyB => { - const currencyA = priceDataA.currencies.find(currencyA => currencyA.id === currencyB.id) ?? { quantity: -Infinity }; - return [currencyA, currencyB]; - }) - return mixedPrices.some(([currencyA, currencyB]) => currencyA.quantity >= currencyB.quantity); +export function isPriceHigherThan(priceDataA, priceDataB) { + if (priceDataA.primary) { + return priceDataA.totalPrice >= priceDataB.totalPrice; + } + const mixedPrices = priceDataB.currencies.map(currencyB => { + const currencyA = priceDataA.currencies.find(currencyA => currencyA.id === currencyB.id) ?? { quantity: -Infinity }; + return [currencyA, currencyB]; + }) + return mixedPrices.some(([currencyA, currencyB]) => currencyA.quantity >= currencyB.quantity); } const DATE_REGEX = new RegExp("^(\\d+)(\\w+)$", "g") export function evaluateFoundryTime(auctioneer, duration = "now") { - const flags = getAuctioneerActorFlags(auctioneer); - - if(flags.timeType === CONSTANTS.AUCTION_TIME_TYPE_KEYS.REAL_TIME || !window?.SimpleCalendar?.api) { - if(duration === "now") return Date.now(); - const parts = [...duration.matchAll(DATE_REGEX)]; - const [_, number, dateType] = parts[0]; - return moment().add(Number(number), dateType).valueOf(); - } - - const currentTimestamp = window?.SimpleCalendar.api.timestamp(); - if(duration === "now") return currentTimestamp; - - const parts = [...duration.matchAll(DATE_REGEX)]; - const [_, number, dateType] = parts[0]; - - const newDateType = { - "minutes": "minute", - "hours": "hour", - "days": "day", - "months": "month", - "years": "year", - }[dateType] ?? dateType; - - return window?.SimpleCalendar.api.timestampPlusInterval(currentTimestamp, { - [newDateType]: Number(number) - }); + const flags = getAuctioneerActorFlags(auctioneer); + + if (flags.timeType === CONSTANTS.AUCTION_TIME_TYPE_KEYS.REAL_TIME || !window?.SimpleCalendar?.api) { + if (duration === "now") return Date.now(); + const parts = [...duration.matchAll(DATE_REGEX)]; + const [_, number, dateType] = parts[0]; + return moment().add(Number(number), dateType).valueOf(); + } + + const currentTimestamp = window?.SimpleCalendar.api.timestamp(); + if (duration === "now") return currentTimestamp; + + const parts = [...duration.matchAll(DATE_REGEX)]; + const [_, number, dateType] = parts[0]; + + const newDateType = { + "minutes": "minute", + "hours": "hour", + "days": "day", + "months": "month", + "years": "year", + }[dateType] ?? dateType; + + return window?.SimpleCalendar.api.timestampPlusInterval(currentTimestamp, { + [newDateType]: Number(number) + }); } -export function dateTimeToString(auctioneer, datetime) { +export function dateTimeToString(auctioneer, datetime, relative = false) { + + const flags = getAuctioneerActorFlags(auctioneer); + + if (flags.timeType === CONSTANTS.AUCTION_TIME_TYPE_KEYS.REAL_TIME || !window?.SimpleCalendar?.api) { + const diffDate = moment(datetime); + return relative ? capitalize(moment.duration(diffDate.diff(moment())).humanize()) : diffDate.format("Y-M-D - HH:mm:ss") + } - const flags = getAuctioneerActorFlags(auctioneer); + if (relative) { + const currentTimestamp = window?.SimpleCalendar.api.timestamp(); + const interval = Object.entries(window?.SimpleCalendar.api.secondsToInterval(datetime - currentTimestamp)).filter(e => e[1]) + const biggestInterval = interval[interval.length - 1]; + let [label, value] = biggestInterval; + label = pluralize(label, value > 1); + return `${value} ${label}` + } - if(flags.timeType === CONSTANTS.AUCTION_TIME_TYPE_KEYS.REAL_TIME || !window?.SimpleCalendar?.api) { - const diffDate = moment(datetime); - return moment.duration(diffDate.diff(moment())).humanize(true) - } + const timestamp = window?.SimpleCalendar.api.timestampToDate(datetime); + return `${timestamp.display.year}-${timestamp.display.month}-${timestamp.display.day} - ${timestamp.display.time}`; - const timestamp = window?.SimpleCalendar.api.timestampToDate(datetime); +} + +function capitalize(str) { + return str.slice(0, 1).toUpperCase() + str.slice(1); +} - return `${timestamp.display.year}-${timestamp.display.month}-${timestamp.display.day} - ${timestamp.display.time}`; +function pluralize(str, doPluralize = true) { + if (doPluralize) { + return str.endsWith("s") ? str : str + "s"; + } + return str.endsWith("s") ? str.slice(0, -1) : str; +} + +export function getUserAuctions(user = false) { + if (!user) user = game.user; + return foundry.utils.deepClone(user.getFlag(CONSTANTS.MODULE_NAME, CONSTANTS.AUCTIONS_FLAG) ?? []); +} + +export function getUserBids(user = false) { + if (!user) user = game.user; + return foundry.utils.deepClone(user.getFlag(CONSTANTS.MODULE_NAME, CONSTANTS.BIDS_FLAG) ?? []); +} +export function getUserBuyouts(user = false) { + if (!user) user = game.user; + return foundry.utils.deepClone(user.getFlag(CONSTANTS.MODULE_NAME, CONSTANTS.BUYOUTS_FLAG) ?? []); } -function pluralize(str, doPluralize = true){ - if(doPluralize) return str.endsWith("s") ? str : str + "s"; - return str.endsWith("s") ? str.slice(0, -1) : str; +export function getItemColorElement(item) { + const color = game.modules.get("rarity-colors")?.active && game.modules.get("rarity-colors")?.api + ? game.modules.get("rarity-colors").api.getColorFromItem(item) + : false; + const rarity = capitalize(CONFIG?.DND5E?.itemRarity?.[item?.system?.rarity] ?? ""); + return color ? ` ` : ""; } -function capitalize(str){ - return str.slice(0,1).toUpperCase() + str.slice(1); +export function evaluateFormula(formula, data, warn = false) { + const rollFormula = Roll.replaceFormulaData(formula, data, { warn }); + return new Roll(rollFormula).evaluate({ async: false }); } -export function getUserAuctions(user = false){ - if(!user) user = game.user; - return foundry.utils.deepClone(user.getFlag(CONSTANTS.MODULE_NAME, CONSTANTS.AUCTIONS_FLAG) ?? []); +export function isActiveGM(user) { + return user.active && user.isGM; } -export function getUserBids(user = false){ - if(!user) user = game.user; - return foundry.utils.deepClone(user.getFlag(CONSTANTS.MODULE_NAME, CONSTANTS.BIDS_FLAG) ?? []); +export function getActiveGMs() { + return game.users.filter(isActiveGM); +} + +export function isResponsibleGM() { + if (!game.user.isGM) { + return false; + } + return !getActiveGMs().some(other => other.id < game.user.id); +} + +function cleanUserFlags(acc, entry, flagPath){ + const userEntries = acc[entry.userId] ?? game.users.get(entry.userId).getFlag(CONSTANTS.MODULE_NAME, flagPath); + if (userEntries.length) { + const auctionIndex = userEntries.findIndex(userAuction => userAuction.uuid === entry.uuid); + userEntries.splice(auctionIndex, 1); + acc[entry.userId] = { + [`flags.${CONSTANTS.MODULE_NAME}.${flagPath}`]: userEntries + }; + } + return acc; +} + +export function getLogs(auctioneer, { auctions = [], bids = [], buyouts = [] } = {}) { + + const claimedAuctions = auctions.filter(auction => auction.claimed).length > 50 + ? auctions.filter(auction => auction.claimed).slice(0, 25).map(auction => auction._source) + : [] + const claimedBids = bids.filter(bid => bid.claimed).length > 50 + ? bids.filter(bid => bid.claimed).slice(0, 25).map(bid => bid._source) + : []; + const claimedBuyouts = buyouts.filter(buyout => buyout.claimed).length > 50 + ? buyouts.filter(buyout => buyout.claimed).slice(0, 25).map(buyout => buyout._source) + : []; + + const maps = { + auctions: {}, + bids: {}, + buyouts: {} + }; + const auctioneerAuctionsData = auctioneer.getFlag(CONSTANTS.MODULE_NAME, CONSTANTS.AUCTIONS_FLAG) ?? []; + const auctioneerAuctions = auctioneerAuctionsData.map(source => { + const auction = makeAuction(auctioneer, source); + maps.auctions[auction.uuid] = auction; + return auction; + }); + + const auctioneerBidsData = auctioneer.getFlag(CONSTANTS.MODULE_NAME, CONSTANTS.BIDS_FLAG) ?? []; + const auctioneerBids = auctioneerBidsData.map(source => { + const bid = makeBid(auctioneer, source, maps.auctions); + maps.bids[bid.id] = bid; + return bid; + }); + + const auctioneerBuyoutsData = auctioneer.getFlag(CONSTANTS.MODULE_NAME, CONSTANTS.BUYOUTS_FLAG) ?? []; + const auctioneerBuyouts = auctioneerBuyoutsData.map(source => { + const buyout = makeBuyout(auctioneer, source, maps.auctions); + maps.buyouts[buyout.id] = buyout; + return buyout; + }); + + const actorUpdates = {}; + if(claimedAuctions.length) { + if(!actorUpdates["_id"]) actorUpdates["_id"] = auctioneer.id; + actorUpdates[CONSTANTS.AUCTIONS_FULL_FLAG] = auctioneerAuctionsData.concat(claimedAuctions) + } + if(claimedBids.length) { + if(!actorUpdates["_id"]) actorUpdates["_id"] = auctioneer.id; + actorUpdates[CONSTANTS.BIDS_FULL_FLAG] = auctioneerBidsData.concat(claimedBids) + } + if(claimedBuyouts.length) { + if(!actorUpdates["_id"]) actorUpdates["_id"] = auctioneer.id; + actorUpdates[CONSTANTS.BUYOUTS_FULL_FLAG] = auctioneerBuyoutsData.concat(claimedBuyouts) + } + + let userUpdates = {}; + const userAuctionUpdates = claimedAuctions.reduce((acc, auction) => { + return cleanUserFlags(acc, auction, CONSTANTS.AUCTIONS_FLAG); + }, {}); + if(!foundry.utils.isEmpty(userAuctionUpdates)) { + userUpdates = foundry.utils.mergeObject(userUpdates, userAuctionUpdates); + } + + const userBidUpdates = claimedBids.reduce((acc, bid) => { + return cleanUserFlags(acc, bid, CONSTANTS.BIDS_FLAG); + }, {}) + if(!foundry.utils.isEmpty(userBidUpdates)) { + userUpdates = foundry.utils.mergeObject(userUpdates, userBidUpdates); + } + + const userBuyoutUpdates = claimedBuyouts.reduce((acc, buyout) => { + return cleanUserFlags(acc, buyout, CONSTANTS.BUYOUTS_FLAG); + }, {}) + if(!foundry.utils.isEmpty(userBuyoutUpdates)) { + userUpdates = foundry.utils.mergeObject(userUpdates, userBuyoutUpdates); + } + + userUpdates = Object.entries(userUpdates).map(([userId, updates]) => { + return { + _id: userId, + ...updates + } + }) + + const currentDatetime = evaluateFoundryTime(auctioneer); + + const auctionLogsMap = auctions + .concat(auctioneerAuctions) + .reduce((acc, auction) => { + if (!acc[auction.id]) { + acc[auction.id] = { + data: auction, + type: "AuctionLog", + id: auction.id, + date: auction.date, + visible: true + }; + } + if (auction.expired && !auction.cancelled) { + if (!auction.won && !acc[auction.id + "-expired"]) { + acc[auction.id + "-expired"] = { + data: auction, + type: "ExpiredAuctionLog", + id: auction.id + "-expired", + date: auction.expiryDate, + visible: true + }; + } + if (auction.claimed && currentDatetime >= auction.claimedDate && !acc[auction.id + "-claimed"]) { + acc[auction.id + "-claimed"] = { + data: auction, + type: "ClaimedAuctionLog", + id: auction.id + "-claimed", + date: auction.claimedDate, + visible: true + }; + } + } + if (auction.cancelled && auction.claimed && currentDatetime >= auction.claimedDate && !acc[auction.id + "-cancelled"]) { + acc[auction.id + "-cancelled"] = { + data: auction, + type: "CancelledAuctionLog", + id: auction.id + "-cancelled", + date: auction.claimedDate, + visible: true + }; + } + return acc; + }, {}) + + const auctionLogs = Object.values(auctionLogsMap); + const bidLogs = Object.values(bids.concat(auctioneerBids).reduce((acc, bid) => { + if(acc[bid.id]) return acc; + acc[bid.id] = { + data: bid, + type: "BidLog", + id: bid.id, + date: bid.date, + visible: true + } + return acc; + }, {})); + const buyoutLogs = Object.values(buyouts.concat(auctioneerBuyouts).reduce((acc, buyout) => { + if(acc[buyout.id]) return acc; + acc[buyout.id] = { + data: buyout, + type: "BuyoutLog", + id: buyout.id, + date: buyout.date, + visible: true + } + return acc; + }, {})); + + return { + logs: auctionLogs + .concat(bidLogs) + .concat(buyoutLogs) + .map((entry, index) => ({ + index, ...entry + })) + .sort((a, b) => { + return b.date === a.date ? b.index - a.index : b.date - a.date; + }), + userUpdates, + actorUpdates + }; + +} + + +export function makeAuction(auctioneer, source) { + const auction = {}; + auction._source = foundry.utils.mergeObject(foundry.utils.deepClone(CONSTANTS.DEFAULTS.AUCTION), source); + auction.type = "auction"; + auction.id = auction._source.id; + auction.uuid = auction._source.uuid; + auction.cancelled = auction._source.cancelled; + auction.claimed = auction._source.claimed; + auction.gmClaimed = auction._source.gmClaimed; + auction.item = new Item.implementation(auction._source.itemData) + auction.user = game.users.get(auction._source.userId); + auction.actor = auction._source.actorUuid ? fromUuidSync(auction._source.actorUuid) : false; + auction.date = auction._source.date; + auction.claimedDate = auction._source.claimedDate; + auction.expiryDate = auction._source.expiryDate; + auction.expired = evaluateFoundryTime(auctioneer) >= auction._source.expiryDate; + auction.timeLeft = dateNumberToRelativeString(auctioneer, auction._source.expiryDate); + auction.quantity = auction._source.quantity; + auction.bidVisibility = auction._source.bidVisibility; + auction.startPrice = auction._source.startPrice; + auction.buyoutPrice = auction._source.buyoutPrice; + auction.reservePrice = auction._source.reservePrice; + auction.minBidPrice = auction._source.minBidPrice; + auction.depositPrice = auction._source.depositPrice; + + auction.startPriceData = getPriceFromData(auction._source.startPrice); + auction.buyoutPriceData = getPriceFromData(auction._source.buyoutPrice); + auction.reservePriceData = getPriceFromData(auction._source.reservePrice); + auction.minBidPriceData = getPriceFromData(auction._source.minBidPrice); + auction.depositPriceData = getPriceFromData(auction._source.depositPrice); + + auction.won = false; + auction.bids = []; + auction.bidPriceData = false; + auction.highestOwnedBid = false; + return auction; } -export function getUserBuyouts(user = false){ - if(!user) user = game.user; - return foundry.utils.deepClone(user.getFlag(CONSTANTS.MODULE_NAME, CONSTANTS.BUYOUTS_FLAG) ?? []); +export function makeBuyout(auctioneer, source, auctions) { + const buyout = {}; + buyout._source = foundry.utils.mergeObject(foundry.utils.deepClone(CONSTANTS.DEFAULTS.BUYOUT), source); + buyout.type = "buyout"; + buyout.id = buyout._source.id; + buyout.date = buyout._source.date; + buyout.user = game.users.get(buyout._source.userId); + buyout.actor = buyout._source.actorUuid ? fromUuidSync(buyout._source.actorUuid) : false; + buyout.priceData = getPriceFromData(buyout._source.price); + buyout.price = buyout._source.price; + buyout.claimed = buyout._source.claimed; + buyout.auctionUuid = buyout._source.auctionUuid; + buyout.auction = auctions[buyout._source.auctionUuid]; + buyout.auction.won = buyout; + return buyout; } -export function getItemColorElement(item){ - const color = game.modules.get("rarity-colors")?.active && game.modules.get("rarity-colors")?.api - ? game.modules.get("rarity-colors").api.getColorFromItem(item) - : false; - const rarity = capitalize(CONFIG?.DND5E?.itemRarity?.[item?.system?.rarity] ?? ""); - return color ? ` ` : ""; +export function makeBid(auctioneer, source, auctions) { + const bid = {}; + bid._source = foundry.utils.mergeObject(foundry.utils.deepClone(CONSTANTS.DEFAULTS.BID), source); + bid.type = "bid"; + bid.id = bid._source.id; + bid.date = bid._source.date; + bid.user = game.users.get(bid._source.userId); + bid.actor = bid._source.actorUuid ? fromUuidSync(bid._source.actorUuid) : false; + bid.priceData = getPriceFromData(bid._source.price); + bid.price = bid._source.price; + bid.claimed = bid._source.claimed; + bid.bidStatus = { value: -Infinity, label: "Low Bid" }; + bid.auctionUuid = bid._source.auctionUuid; + bid.auction = auctions[bid._source.auctionUuid]; + bid.auction.bids.push(bid); + return bid; } diff --git a/src/module.js b/src/module.js index bdd48e1..8f94eab 100644 --- a/src/module.js +++ b/src/module.js @@ -1,9 +1,48 @@ import "./styles/styles.scss"; import CONSTANTS from "./constants.js"; import * as lib from "./lib.js"; +import { getAuctioneerActorData } from "~/applications/auctioneer/auctioneer-store.js"; Hooks.once("ready", () => { document.documentElement.style.setProperty("--item-piles-auctioneer-ui-height", `${CONSTANTS.AUCTION_UI_HEIGHT}px`); Hooks.on(game.itempiles.hooks.PRE_RENDER_INTERFACE, lib.auctioneerRendered); game.itempiles.API.registerItemPileType(CONSTANTS.AUCTIONEER, "Auctioneer", CONSTANTS.AUCTIONEER_SETTINGS); + migrateData(); }); + +async function migrateData() { + if(!lib.isResponsibleGM()) return; + const auctioneers = game.actors + .filter(actor => { + return actor.getFlag("item-piles", 'data')?.type === CONSTANTS.AUCTIONEER; + }) + .map(auctioneer => { + const auctioneerData = getAuctioneerActorData(auctioneer); + const { userUpdates, actorUpdates } = lib.getLogs(auctioneer, auctioneerData); + return { + id: auctioneer.id, + userUpdates, + actorUpdates + } + }) + .filter(auctioneer => { + return !foundry.utils.isEmpty(auctioneer.actorUpdates) || auctioneer.userUpdates.length; + }); + if(!auctioneers.length) return; + + const actorUpdates = auctioneers.map(auctioneer => auctioneer.actorUpdates); + const userUpdates = auctioneers.map(auctioneer => auctioneer.userUpdates) + .deepFlatten() + .reduce((acc, update) => { + const foundIndex = acc.findIndex(pastUpdate => pastUpdate._id === update._id); + if(foundIndex > -1){ + acc[foundIndex] = foundry.utils.mergeObject(acc[foundIndex], update); + }else{ + acc.push(update); + } + return acc; + }, []); + + await Actor.updateDocuments(actorUpdates) + await User.updateDocuments(userUpdates); +} diff --git a/src/styles/styles.scss b/src/styles/styles.scss index bb9d578..e17ef58 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -34,6 +34,20 @@ padding: 0.15rem; --tjs-app-overflow: visible; + &.dialog .dialog-content { + text-align: center; + + p { + margin-top: 0; + } + + .dialog-center { + display: flex; + align-items: center; + justify-content: center; + } + } + & > section { border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; @@ -49,6 +63,20 @@ } } + .access-denied { + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.5); + color: white; + border-radius: 5px; + z-index: 50; + left: 0.55rem; + right: 0.55rem; + top: 30px; + bottom: 42px; + } + .character-bottom-bar { display: flex; align-items: center; @@ -274,7 +302,7 @@ display: flex; flex-direction: column; height: calc(var(--item-piles-auctioneer-ui-height) - 84px); - padding: 0.25rem; + padding: 0.25rem 0 0.25rem 0.25rem; border: var(--item-piles-auctioneer-border); border-radius: var(--item-piles-auctioneer-border-radius); @@ -286,12 +314,19 @@ max-height: calc(var(--item-piles-auctioneer-ui-height) - 123px); overflow-x: hidden; overflow-y: auto; + padding-right: 0.25rem; .auction-title { display: flex; justify-content: center; align-items: center; width: 100%; + + &.small-warning{ + font-size: 0.75rem; + color: #640000; + text-align: center; + } } .auction-visibility { @@ -352,13 +387,18 @@ .stack-container { display: grid; grid-template-columns: 1fr 40px 45px; - margin: 0.25rem 0; + margin: 0.15rem 0; grid-gap: 0.15rem; text-align: center; & > * { margin: 0; } + + input, button { + height: 20px; + line-height: 0; + } } .auction-duration { @@ -368,6 +408,16 @@ } } + .auction-deposit { + display: flex; + flex-direction: column; + + .cant-afford { + color: #8c0e0e; + font-weight: bold; + } + } + .price-currencies-container-pair { display: grid; grid-template-columns: 1fr 1fr; @@ -441,8 +491,14 @@ } } - .auction-post-button { + .auction-post-buttons { + display: flex; + flex-direction: column; margin-top: 0.25rem; + + button { + margin: 0; + } } }