v13.0.24 - Le grand oubli d'Illysis #789

Merged
giteadmin merged 5 commits from feature/v13-corrections into v13 2025-12-20 17:21:04 +01:00
17 changed files with 246 additions and 46 deletions

View File

@@ -1,5 +1,18 @@
# 13.0
## 13.0.24 - Le grand oubli d'Illysis
- ajout d'un bouton pour supprimer les anciens messages du tchat
- correction de messages d'erreur 'User ... lacks permission' / 'Active Effect ... does not exist'
- les joueurs peuvent affecter leurs enchantements aux objets enchantables de leur équipement
- correction des boutons pour lancer les maladresses
- utilisation des tables de compendium
- correction des droits pour mise à jour du message
- correction de la cuisine
- reprise de l'image de cuisine dans l'équipement
- prise en compte de la difficulté pour préparer la nourriture brute
## 13.0.23 - Le marché d'Illysis
- Améliorations

View File

@@ -1528,6 +1528,7 @@ body {
}
.system-foundryvtt-reve-de-dragon .item-actions-controls img,
.system-foundryvtt-reve-de-dragon .item-controls img {
vertical-align: text-bottom;
display: inline;
max-width: 1rem;
max-height: 1rem;
@@ -1540,10 +1541,17 @@ body {
font-size: 0.8em;
color: var(--color-controls-light);
}
.system-foundryvtt-reve-de-dragon .item-actions-controls img:hover,
.system-foundryvtt-reve-de-dragon .item-controls img:hover,
.system-foundryvtt-reve-de-dragon .item-actions-controls i:is(.fas, .far, .fa-solid, .fa-regular):hover,
.system-foundryvtt-reve-de-dragon .item-controls i:is(.fas, .far, .fa-solid, .fa-regular):hover {
.system-foundryvtt-reve-de-dragon .item-actions-controls a:hover,
.system-foundryvtt-reve-de-dragon .item-controls a:hover {
text-shadow: 1px 0px 0px #ff6600;
}
.system-foundryvtt-reve-de-dragon .item-actions-controls a img:hover,
.system-foundryvtt-reve-de-dragon .item-controls a img:hover {
opacity: 0.6;
filter: drop-shadow(1px 1px 1px #009966) invert(0.8);
}
.system-foundryvtt-reve-de-dragon .item-actions-controls a i:is(.fas, .far, .fa-solid, .fa-regular):hover,
.system-foundryvtt-reve-de-dragon .item-controls a i:is(.fas, .far, .fa-solid, .fa-regular):hover {
opacity: 0.6;
}
.system-foundryvtt-reve-de-dragon .rdd-roll-dialog .description-sort {

View File

@@ -852,6 +852,7 @@
// }
img {
vertical-align: text-bottom;
display: inline;
max-width: 1rem;
max-height: 1rem;
@@ -865,8 +866,16 @@
color: var(--color-controls-light);
}
img:hover,
i:is(.fas, .far, .fa-solid, .fa-regular):hover {
a:hover {
text-shadow: 1px 0px 0px #ff6600;
}
a img:hover{
opacity: 0.6;
filter: drop-shadow(1px 1px 1px #009966) invert(0.8);
}
a i:is(.fas, .far, .fa-solid, .fa-regular):hover {
opacity: 0.6;
}
}

View File

@@ -5,7 +5,7 @@ import { RdDTextEditor } from "./apps/rdd-text-roll-editor.js";
/**
* Class providing helper methods to get the list of users, and
* Class providing helper methods around the Chat message
*/
export class ChatUtility {
@@ -233,7 +233,12 @@ export class ChatUtility {
static async setTimestamp(chatMessage) {
await chatMessage.setFlag(SYSTEM_RDD, 'rdd-timestamp', game.system.rdd.calendrier.getTimestamp());
}
// TODO: find ChatLog to change the action for data-action flush
static getISODate(chatMessage) {
const date = new Date(chatMessage.timestamp);
return date?.toISOString().substring(0, 10)
}
// async flush() {
// const question = game.i18n.localize("AreYouSure");

View File

@@ -0,0 +1,79 @@
import { ChatUtility } from "../chat-utility.js"
import { Misc } from "../misc.js"
import { RdDTimestamp } from "../time/rdd-timestamp.js"
const fields = foundry.applications.fields
export class DialogFlushByDate {
static async init() {
Hooks.on("renderChatMessageHTML", async (app, html, msg) => await ChatUtility.onRenderChatMessage(app, html, msg))
Hooks.on("createChatMessage", async (chatMessage, options, id) => await ChatUtility.onCreateChatMessage(chatMessage, options, id))
Hooks.once("renderChatLog", async () => await DialogFlushByDate.onFirstRenderChatLog())
}
static async onFirstRenderChatLog() {
if (game.user.isGM) {
const content = await foundry.applications.handlebars.renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat/button-flush-by-date.hbs')
const flushButton = document.querySelector("#chat-controls button[data-action='flush']")
flushButton.insertAdjacentHTML("afterend", content)
flushButton.parentElement.querySelector("button[name='flush-by-date']")?.addEventListener(
"click", e => {
if (game.messages.size > 0) {
DialogFlushByDate.create()
}
else {
ui.notifications.info("Aucun message à supprimer!")
}
e.preventDefault()
})
}
}
static async create() {
const dates = DialogFlushByDate.getChatMessageDates(game.messages)
const selectGroup = fields.createFormGroup({
input: fields.createSelectInput({
options: dates.map(it => { return { label: `${it.dateTerre} | ${it.dateReve}`, value: it.timestamp } }),
name: 'timestamp'
}),
label: 'Messages antérieurs au',
hint: 'Supprimer les messages antérieurs à la date'
})
const selected = await foundry.applications.api.DialogV2.input({
window: { title: "Supprimer les messages du tchat" },
content: selectGroup.outerHTML,
ok: {
label: "Supprimer",
icon: "fa-solid fa-trash-can",
}
})
if (selected) {
const timestamp = Number.parseInt(selected.timestamp)
const toDelete = game.messages.filter(it => it.timestamp < timestamp)
toDelete.forEach(it => it.delete())
}
}
static getChatMessageDates(messages) {
const datesTerre = Object.values(Misc.classifyFirst(messages,
m => ChatUtility.getISODate(m),
m => DialogFlushByDate.chatMessageDates(m)))
const timestamps = new Set(datesTerre.map(it => it.timestamp))
const datesReve = Object.values(Misc.classifyFirst(messages,
m => RdDTimestamp.fromChatMessage(m).isoDate(),
m => DialogFlushByDate.chatMessageDates(m)))
const dates = [...datesTerre, ...datesReve.filter(it => !timestamps.has(it.timestamp))]
return dates.sort(Misc.ascending(it => it.timestamp))
}
static chatMessageDates(it) {
return {
timestamp: it.timestamp,
dateTerre: new Date(it.timestamp).toLocaleDateString(),
dateReve: RdDTimestamp.fromChatMessage(it).formatDate()
}
}
}

View File

@@ -1,10 +1,10 @@
import { ITEM_TYPES, renderTemplate } from "../constants.js"
import { ACTOR_TYPES, ITEM_TYPES, renderTemplate } from "../constants.js"
import { RdDItemSort } from "../item-sort.js"
import { Misc } from "../misc.js"
export const ACTION_ITEM_ENCHANTER = {
code: 'item-enchanter', label: 'Enchanter', icon: it => 'fa-solid fa-sparkles',
filter: it => game.user.isGM || DialogEnchanter.isEnchantable(it) && it.parent?.type != ACTOR_TYPES.commerce,
filter: it => DialogEnchanter.isEnchantable(it) && it.parent?.type != ACTOR_TYPES.commerce,
action: (item, actor) => DialogEnchanter.enchanter(item)
}

View File

@@ -48,7 +48,9 @@ const _EQUIPER = {
}
const _CUISINER = {
code: 'item-cuisiner', label: 'Cuisiner', icon: it => 'fa-solid fa-utensils',
code: 'item-cuisiner', label: 'Cuisiner',
img: it => 'systems/foundryvtt-reve-de-dragon/assets/actions/cuisine.svg',
// icon: it => 'fa-solid fa-spoon',
filter: it => it.getUtilisation() == 'cuisine' && it.system.sust > 0,
action: (item, actor) => actor.preparerNourriture(item)
}
@@ -94,7 +96,7 @@ const _SORT_RESERVE = {
}
export const COMMON_ACTIONS = [_EQUIPER]
export const DEFAULT_ACTIONS = [_ACHETER, _SPACEHOLDER, _SPLIT, _VENDRE, _MONTRER, _EDIT, _DELETE]
export const DEFAULT_ACTIONS = [_ACHETER, _SPLIT, _SPACEHOLDER, _VENDRE, _MONTRER, _EDIT, _DELETE]
export const ITEM_ACTIONS = {
faune: [_CUISINER, _MANGER_CRU],
@@ -119,11 +121,18 @@ export class ItemAction {
&& (!action.optionsFilter || action.optionsFilter(options))
}
static icon(action, item) {
if (action && action.icon) {
return action.icon(item)
static img(action, item) {
if (action.placeholder){
return ""
}
return undefined
if (action?.img) {
return `<img src="${action.img(item)}" />`
}
if (action?.icon) {
return `<i class="${action.icon(item)}"></i>`
}
return action.label
}
static async onActionItem(event, actor, options) {

View File

@@ -134,12 +134,12 @@ export class Misc {
return itemsBy
}
static classifyFirst(items, classifier) {
static classifyFirst(items, classifier, classified = it => it) {
let itemsBy = {};
for (const item of items) {
const classification = classifier(item);
if (!itemsBy[classification]) {
itemsBy[classification] = item;
itemsBy[classification] = classified(item);
}
}
return itemsBy;
@@ -180,11 +180,15 @@ export class Misc {
return (a, b) => a + separator + b;
}
static connectedGMOrUser(ownerId = undefined) {
if (ownerId && game.user.id == ownerId) {
return ownerId;
static connectedUserOrGM(userId = undefined) {
if (userId && game.user.id == userId) {
return userId
}
return Misc.firstConnectedGM()?.id ?? game.user.id;
return Misc.firstConnectedGM()?.id
}
static connectedGMOrUser(userId = undefined) {
return Misc.connectedUserOrGM(userId) ?? game.user.id
}
static isRollModeHiddenToPlayer() {
@@ -220,8 +224,10 @@ export class Misc {
* and there is no connected GM
*/
static documentIfResponsible(document) {
if (game.users.activeGM || (Misc.connectedGMs().length == 0 && Misc.isFirstOwnerPlayer(document))) {
return document
if (Misc.isFirstConnectedGM() || (Misc.connectedGMs().length == 0 && Misc.isFirstOwnerPlayer(document))) {
if (document.isOwner) {
return document
}
}
return undefined
}
@@ -230,16 +236,16 @@ export class Misc {
if (Misc.isOwnerPlayer(document)) {
return await action(document)
} else {
return await orElse(document ?? {name: '<aucune sélection>'})
return await orElse(document ?? { name: '<aucune sélection>' })
}
}
static isOwnerPlayer(document) {
return document && document.testUserPermission && document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)
return document?.isOwner
}
static isFirstOwnerPlayer(document) {
if (!document?.testUserPermission) {
if (!document?.isOwner) {
return false
}
return game.users.find(u => document.testUserPermission(u, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) == game.user
@@ -249,7 +255,7 @@ export class Misc {
* @returns true pour un seul utilisateur: le premier GM connecté par ordre d'id
*/
static isFirstConnectedGM() {
return game.user == game.users.activeGM
return game.users.activeGM != undefined && game.user == game.users.activeGM
}
static hasConnectedGM() {

View File

@@ -90,6 +90,8 @@ import { Migrations } from './migrations.js'
import RollDialog from "./roll/roll-dialog.mjs"
import ChatRollResult from "./roll/chat-roll-result.mjs"
import ExportPdf from "./actor/export-pdf/export-pdf.mjs"
import { DialogFlushByDate } from "./chat/dialog-flush-by-date.mjs"
import { Remote } from "./remote.mjs"
/**
* RdD system
@@ -181,6 +183,7 @@ export class SystemReveDeDragon {
game.socket.on(SYSTEM_SOCKET_ID, async (sockmsg) => {
console.log(">>>>> MSG RECV", sockmsg)
try {
Remote.onSocketMessage(sockmsg)
RdDUtility.onSocketMessage(sockmsg)
RdDCombat.onSocketMessage(sockmsg)
ChatUtility.onSocketMessage(sockmsg)
@@ -300,6 +303,7 @@ export class SystemReveDeDragon {
ExportPdf.init()
RollDialog.init()
ChatRollResult.init()
DialogFlushByDate.init()
}
initSettings() {

View File

@@ -348,7 +348,7 @@ export class RdDUtility {
// Items
Handlebars.registerHelper('rarete-getChamp', (rarete, field) => RdDRaretes.getChamp(rarete, field));
Handlebars.registerHelper('item-action-applies', (action, item, options) => ItemAction.applies(action, item, options))
Handlebars.registerHelper('item-action-icon', (action, item) => ItemAction.icon(action, item))
Handlebars.registerHelper('item-action-img', (action, item) => new Handlebars.SafeString(ItemAction.img(action, item)))
Handlebars.registerHelper('item-name', (item) => item.nameDisplay)
// TMRs

53
module/remote.mjs Normal file
View File

@@ -0,0 +1,53 @@
import { RdDBaseActor } from "./actor/base-actor.js";
import { SYSTEM_SOCKET_ID } from "./constants.js";
import { Misc } from "./misc.js";
export class Remote {
static onSocketMessage(sockmsg) {
switch (sockmsg.msg) {
case "msg_remote_call":
return Remote.onRemoteCall(sockmsg.userId, sockmsg.data);
}
}
static remoteCall({ userId = undefined, documentName, documentId, tokenId = undefined, method, args }) {
const callData = { documentName, documentId, tokenId, method, args }
const sendToUserId = Misc.connectedUserOrGM(userId)
if (sendToUserId == undefined) {
ui.notifications.error(`Impossible d'envoyer un message à ${userId}`)
}
else if (sendToUserId == game.user.id) {
Remote.onRemoteCall(game.user.id, callData)
return false
}
else {
game.socket.emit(SYSTEM_SOCKET_ID, { msg: "msg_remote_call", userId: sendToUserId, data: callData })
return true
}
}
static onRemoteCall(userId, callData) {
if (userId == game.user.id) {
// Seul le joueur choisi effectue l'appel: le joueur courant si propriétaire de l'actor, ou le MJ sinon
const doc = Remote.getDoc(callData)
if (doc) {
const args = callData.args;
const method = callData.method;
console.info(`Remote.onRemoteCall: ${callData.documentName} ${callData.documentId}, appel de ${method}(`, ...args, ')')
doc[method](...args)
}
}
}
static getDoc(callData) {
switch (callData.documentName) {
case 'Actor': return RdDBaseActor.getRealActor(callData.documentId, callData.tokenId)
case 'Item': return game.items.get(callData.documentId)
case 'RollTable': return game.tables.get(callData.documentId)
case 'ChatMessage': return game.messages.get(callData.documentId)
}
return undefined
}
}

View File

@@ -16,6 +16,7 @@ import { Misc } from "../misc.js"
import { RollBasicParts } from "./roll-basic-parts.mjs"
import { RdDPossessionV2 } from "../rdd-possession-v2.mjs"
import { Apprecier } from "../moral/apprecier.mjs"
import { Remote } from "../remote.mjs"
export default class ChatRollResult {
static init() {
@@ -168,14 +169,21 @@ export default class ChatRollResult {
async updateChatMessage(chatMessage, savedRoll) {
RollDialog.loadRollData(savedRoll)
savedRoll.dmg = savedRoll.current.attaque?.dmg
await this.saveChatMessageRoll(chatMessage, savedRoll)
this.prepareDisplay(savedRoll)
chatMessage.update({ content: await this.buildRollHtml(savedRoll) })
chatMessage.render(true)
const content = await this.buildRollHtml(savedRoll)
Remote.remoteCall({
userId: chatMessage.author.id,
documentName: chatMessage.documentName,
documentId: chatMessage.id,
method: 'update',
args: [{ content: content }, { render: true }]
})
}
async saveChatMessageRoll(chatMessage, roll, impacts = undefined) {
const save = RollDialog.saveParts(roll, impacts)
await ChatUtility.setMessageData(chatMessage, 'rollData', save)

View File

@@ -150,12 +150,13 @@ export class RollPartCuisine extends RollPartSelect {
this.$selectPreparation(rollDialog.rollData, selectOptions[index]?.value)
rollDialog.render()
})
checkboxFabriquer?.addEventListener("change", e => {
current.fabriquer = e.currentTarget.checked
})
inputDiff?.addEventListener("change", e => {
current.value = parseInt(e.currentTarget.value)
rollDialog.render()
})
inputProportions?.addEventListener("change", e => {
current.proportions = parseInt(e.currentTarget.value)

View File

@@ -7,7 +7,7 @@ import { RollType } from "./roll-type.mjs"
export class RollTypeCuisine extends RollType {
get code() { return ROLL_TYPE_CUISINE }
get name() { return `Interpréter une oeuvre` }
get name() { return `Cuisiner un plat` }
visible(rollData) { return rollData.active.actor.isPersonnage() }
title(rollData) {

View File

@@ -222,6 +222,9 @@ export class RdDTimestamp {
})
}
static fromChatMessage(chatMessage) {
return new RdDTimestamp(chatMessage.getFlag(SYSTEM_RDD, 'rdd-timestamp'))
}
/**
* Constructeur d'un timestamp.
* Selon les paramètres, l'objet construit se base su:
@@ -231,7 +234,7 @@ export class RdDTimestamp {
* @param indexMinute: la minute de la journée à utiliser pour ce timestamp
*
*/
constructor({ indexDate, indexMinute = undefined }) {
constructor({ indexDate = 0, indexMinute = undefined }) {
this.indexDate = indexDate
this.indexMinute = indexMinute ?? 0
}
@@ -269,6 +272,10 @@ export class RdDTimestamp {
};
}
isoDate() {
return `${this.annee.paddedString(4)}-${this.mois.paddedString(2)}-${this.jour.paddedString(2)}`
}
formatDate() {
const jour = this.jour + 1;
const mois = RdDTimestamp.definition(this.mois).label;

View File

@@ -1,15 +1,9 @@
<span class="item-actions-controls item-controls">
{{#each item.actions as |action|}}
{{#if action.placeholder}}
&nbsp;
{{/if}}
{{#if (item-action-applies action ../item ../options)}}
{{#if action.placeholder}}&nbsp;
{{else if (item-action-applies action ../item ../options)}}
<a class="actionItem" data-tooltip="{{action.label}}" data-code="{{action.code}}">
{{#if (item-action-icon action ../item)}}
<i class="{{item-action-icon action ../item}}"></i>
{{else}}
{{action.label}}
{{/if}}
{{item-action-img action ../item}}
</a>
{{/if}}
{{/each}}

View File

@@ -0,0 +1,4 @@
<button type="button" class="ui-control icon fa-solid fa-trash-can" name="flush-by-date"
data-tooltip=""
aria-label="Effacer le tchat par date"
data-action="flush-by-date"></button>