Can the application to respond reactively to game.combat?

I tried this:
{{#if game.combat}}
<span class="ml-sm">
<Badge type="{{badgeType item}}">{{remaining item}}</Badge>
</span>
{{/if}}
{{#if game.combat}}
<span class="ml-sm">
<Badge type="{{badgeType item}}">{{remaining item}}</Badge>
</span>
{{/if}}
But it's not reactive. I also tried:
<script>
$: isCombat = game.combat
<script>
<template>
{{#if isCombat}}
<span class="ml-sm">
<Badge type="{{badgeType item}}">{{remaining item}}</Badge>
</span>
{{/if}}
</template>
<script>
$: isCombat = game.combat
<script>
<template>
{{#if isCombat}}
<span class="ml-sm">
<Badge type="{{badgeType item}}">{{remaining item}}</Badge>
</span>
{{/if}}
</template>
And also tried with game.combat.active, which provides a boolean value, but still not reactively.
9 Replies
TyphonJS (Michael)
That's not how things work.. I'm cooking up a starter resource for you though that should get things rolling.
Vauxs
Vauxs4d ago
You need to create a hook if TJS doesn't provide any helpers. In order to get or use a reactive value it needs to be reactive from the beginning. Foundry inherently has 0 reactivity. Otherwise best you can do is listen for any changes with hooks and modify your own reactive values.
TyphonJS (Michael)
I'm working on using TJSDocumentCollection / TJSDocument. No hooks needed per se. Though this does bring up the v12 situation with the annoyance of renderContext vs action in update data. The demo code I'll provide uses both of those, but I'll look into making a change for TRL to provide a standardized action.
Vauxs
Vauxs4d ago
Neat
TyphonJS (Michael)
It's super frustrating that the document model is so lame. I have tested TJSDocumentCollection against macros. When a macro is created the DB operation has render: true thus TJSDocumentCollection receives a notification when a macro is added / created. When a combat is created from a click of the combat icon from the token HUD the DB operation includes render: false and no notification is posted. When an empty combat is created from the combat tracker itself hitting the + icon render: true is the DB operation thus TJSDocumentCollection receives a callback on combat encounter creation. The whole registering apps directly with documents & collections is so ghetto and is what really should be fixed / updated. It's a weak sauce implementation from the early days of Foundry that hasn't changed except for progressively altering data in the callbacks. It's not good programming. Unfortunately TRL has only one way to receive callbacks from the document model and it's this antiquated and unevenly distributed mechanism that isn't the product of good quality design / implementation. So yeah... You can create a reactive data source for the combats collection, but it's super hacky and does require the use of at least one hook due to the render: false coming in from the token HUD initiation of a combat. The Combat document itself manually invokes ui.combat.render() when combatants are added which is the path taken when clicking on the token hud to add combatants. The problem with creating some sort of standard resource like a reactive combat tracker for TRL is that any of this can be tweaked / changed for any Foundry version and likely is already for v13. I thought there might be a problem in my code, but there is not. I also happened to be using the token HUD to start a combat the entire time in testing then spent ~2 hours debugging / following the path through Foundry to ensure it wasn't a bug in TJSDocumentCollection. Here is the code in TokenDocument that creates a combat with render: false:
if ( game.user.isGM ) {
const cls = getDocumentClass("Combat");
combat = await cls.create({scene: canvas.scene.id, active: true}, {render: false});
}
if ( game.user.isGM ) {
const cls = getDocumentClass("Combat");
combat = await cls.create({scene: canvas.scene.id, active: true}, {render: false});
}
Search for getDocumentClass("Combat") and you'll see that combats are created at 3 points.. Only the instance from the token HUD is render: false. In DocumentCollection here is _onModifyContents:
_onModifyContents(action, documents, result, operation, user) {
if ( operation.render ) {
this.render(false, {renderContext: `${action}${this.documentName}`, renderData: result});
}
}
_onModifyContents(action, documents, result, operation, user) {
if ( operation.render ) {
this.render(false, {renderContext: `${action}${this.documentName}`, renderData: result});
}
}
this.render invokes render on registered apps of the collection which is the only way to register for callbacks. A better way of doing this is just making the documents & collections subscribable. There are many reasons why one would want to receive an update. --- Heh.. Just venting above... I was hoping my demo code would have been clean by just using TJSDocumentCollection / TJSDocument alone and no hooks. The combat tracker code is a mess IMHO. It's a patchwork implementation. Activation of a Combat document also uses render: false, so makes it a bit nebulous on providing a clean reactive source. It's possible, but I'd be handing you a very inefficient foot gun. game.combat queries the UI / CombatTracker as things go. Basically the "reactive" version of getting the actively viewed combat would have to check this every single time there was a change to any combat encounter for any reason. Not efficient. OK... So... The problem is gnarly if one is approaching this from a non system dev situation... I assume this is for your own game system. What you can do is extend the CombatTracker class and override initialize.
class MyCombatTracker extends CombatTracker {
initialize(options) {
super.initialize(options);

// Set a store here.
viewedCombat.set(this.viewed);
}
}

Hooks.once('init', () =>
{
CONFIG.ui.combat = MyCombatTracker;
});
class MyCombatTracker extends CombatTracker {
initialize(options) {
super.initialize(options);

// Set a store here.
viewedCombat.set(this.viewed);
}
}

Hooks.once('init', () =>
{
CONFIG.ui.combat = MyCombatTracker;
});
This does work... Not pretty... What game.combat does is invoke ui.combat?.viewed ?? null. ---- There is really no clean way to do this otherwise from a generic / external way that doesn't involve a custom combat tracker. If this is for your game system then that is the best route. The Foundry core implementation hard coded this path to the UI / combat tracker. TJSDocument / TJSDocumentCollection can't be used reliably because of the render: false document updates.
geoidesic
geoidesicOP3d ago
That works thank you and it's a lot cleaner than what I was doing which was subscribing and unsubscribing to hooks in my component's onMount.
let combat;

function onCombatUpdate() {
combat = game.combat;
}

onMount(() => {
// Initial combat state
combat = game.combat;

// Subscribe to combat updates
Hooks.on('createCombat', onCombatUpdate);
Hooks.on('deleteCombat', onCombatUpdate);
Hooks.on('updateCombat', onCombatUpdate);

return () => {
// Cleanup hooks on component destroy
Hooks.off('createCombat', onCombatUpdate);
Hooks.off('deleteCombat', onCombatUpdate);
Hooks.off('updateCombat', onCombatUpdate);
};
});
let combat;

function onCombatUpdate() {
combat = game.combat;
}

onMount(() => {
// Initial combat state
combat = game.combat;

// Subscribe to combat updates
Hooks.on('createCombat', onCombatUpdate);
Hooks.on('deleteCombat', onCombatUpdate);
Hooks.on('updateCombat', onCombatUpdate);

return () => {
// Cleanup hooks on component destroy
Hooks.off('createCombat', onCombatUpdate);
Hooks.off('deleteCombat', onCombatUpdate);
Hooks.off('updateCombat', onCombatUpdate);
};
});
I'm having an axiomatic problem within my combat-tracker.js extension however, which is that I can't find a way to access actor within it in a reactive way.
TyphonJS (Michael)
The above snippet you posted also wouldn't work well because you can create multiple combats on a scene. Are you talking about embedded documents on the combat doc in regard to actors?
geoidesic
geoidesicOP3d ago
Not sure. I'd be using this:
const combatant = game.combat.combatants.get(turn.id);
const actor = combatant.actor
const combatant = game.combat.combatants.get(turn.id);
const actor = combatant.actor
But I think I might be wrong and it is reactive, I think I'm having some race conditions with how I'm updating the actor
TyphonJS (Michael)
That you can use TJSDocument for and assign it the viewed combat. Pseudocode ahead. This will work if there is an existing combat on load:
import { TJSDocument } from '#runtime/svelte/store/fvtt/document';

Hooks.once('ready', () =>
{
const doc = new TJSDocument(game.combat);

const combatants = doc.embedded.create(Combatant, {
sort: (a, b) => a.name.localeCompare(b.name)
});

doc.subscribe(() => void 0);

combatants.subscribe((values) =>
{
console.log(`Combatants ------`);
for (const combatant of values)
{
console.log(combatant.name);
}
});
});
import { TJSDocument } from '#runtime/svelte/store/fvtt/document';

Hooks.once('ready', () =>
{
const doc = new TJSDocument(game.combat);

const combatants = doc.embedded.create(Combatant, {
sort: (a, b) => a.name.localeCompare(b.name)
});

doc.subscribe(() => void 0);

combatants.subscribe((values) =>
{
console.log(`Combatants ------`);
for (const combatant of values)
{
console.log(combatant.name);
}
});
});
Don't write code like this and not unsubscribe / destroy the listeners when using the JS API. This demo did bring up an item on the TODO list. The added automatic sub / unsub to the underlying document can get in the way if all you want to do is monitor embedded collections. IE see the doc.subscribe(() => void 0);... That is there to subscribe to the combat doc which feeds the embedded collection updates as well. I have to consider that the main document should potentially be subscribed to if there are any embedded collections created. Basically TJSDocument / TJSDocumentCollection is awaiting a thorough refactor after the Svelte 5 transition. It's like version 1.1. So... Instead of using a store You can do something _LIKE this... A bit closer than the previous scratch demo above.
class MyCombatTracker extends CombatTracker {
#combat;
#combatants;

constructor() {
this.#combat = new TJSDocument();
this.#combatants = this.#combat.embedded.create(Combatant, {
sort: (a, b) => a.name.localeCompare(b.name)
});

// This will create a permanent listener. So the underlying doc
// will always be subscribed to on change.
this.#combat.subscribe(() => void 0);
}

initialize(options) {
super.initialize(options);

// Set the document here. Must coerce to `undefined` instead of `null`
this.#combat.set(this.viewed ?? void 0);
}
}
class MyCombatTracker extends CombatTracker {
#combat;
#combatants;

constructor() {
this.#combat = new TJSDocument();
this.#combatants = this.#combat.embedded.create(Combatant, {
sort: (a, b) => a.name.localeCompare(b.name)
});

// This will create a permanent listener. So the underlying doc
// will always be subscribed to on change.
this.#combat.subscribe(() => void 0);
}

initialize(options) {
super.initialize(options);

// Set the document here. Must coerce to `undefined` instead of `null`
this.#combat.set(this.viewed ?? void 0);
}
}
Alrighty.... Here is some tested code for you to try.. I found a bug in the Foundry CombatTracker. This prevents the usage of private variables / methods in an overridden initialize implementation. See the queueMicrotask used. In stress testing with multiple combats and switching between them I also found a bug in the dynamic reducer Map implementation which is how "reactive embedded collections" work. It only occurs when you only have just a sort function applied and switch between documents (IE different combats). The demo code below does not have a sort function. I will have this fixed next TRL release. 100% test coverage didn't find that one, so glad I spent the time today to try and help you. I also refined TJSDocument / TJSDocumentCollection allowing undefined & null to be used in set to unset the tracked document and some refinements to the embedded API. I will follow up in this forum post after the next TRL release with more streamlined code example.
class MyCombatTracker extends CombatTracker
{
#combat;
#combatants;

constructor(options)
{
super(options);

this.#combat = new TJSDocument();

// For the time being until the next TRL release don't add just a sort
// function as there is an edge case I'm fixing.
this.#combatants = this.#combat.embedded.create(Combatant, 'combatants');

// This will create a permanent listener to TJSDocument so the combatants
// embedded collection will always receive updates on change. This is safe
// in this context, but be wary of doing this adhoc.
this.#combat.subscribe(() => void 0);

this.#combatants.subscribe((values) =>
{
console.log(`Combatants ------`);
for (const combatant of values)
{
console.log(combatant.name);
}
});
}

initialize(options)
{
super.initialize(options);

// Use a microtask due to Foundry CombatTracker ctor bug. You can't use
// private variables here. This is just a workaround until fixed
// in Foundry core.

// Set the document here. Must coerce to `undefined` instead of `null`.
// In the next TRL update TJSDocument.set will also allow `null` here
// to clear the document.
queueMicrotask(() => this.#combat.set(this.viewed ?? void 0));
}
}

Hooks.once('init', () =>
{
CONFIG.ui.combat = MyCombatTracker;
});
class MyCombatTracker extends CombatTracker
{
#combat;
#combatants;

constructor(options)
{
super(options);

this.#combat = new TJSDocument();

// For the time being until the next TRL release don't add just a sort
// function as there is an edge case I'm fixing.
this.#combatants = this.#combat.embedded.create(Combatant, 'combatants');

// This will create a permanent listener to TJSDocument so the combatants
// embedded collection will always receive updates on change. This is safe
// in this context, but be wary of doing this adhoc.
this.#combat.subscribe(() => void 0);

this.#combatants.subscribe((values) =>
{
console.log(`Combatants ------`);
for (const combatant of values)
{
console.log(combatant.name);
}
});
}

initialize(options)
{
super.initialize(options);

// Use a microtask due to Foundry CombatTracker ctor bug. You can't use
// private variables here. This is just a workaround until fixed
// in Foundry core.

// Set the document here. Must coerce to `undefined` instead of `null`.
// In the next TRL update TJSDocument.set will also allow `null` here
// to clear the document.
queueMicrotask(() => this.#combat.set(this.viewed ?? void 0));
}
}

Hooks.once('init', () =>
{
CONFIG.ui.combat = MyCombatTracker;
});
Yeah.. You can see Atro's response to the issue I filed. I don't consider Atro to be, well, as informed as one would hope per se. queueMicrotask is the appropriate solution in core. I'm also going to be evaluating areas in TRL where I've used setTimeout(() => {}, 0) and swap to queueMicrotask where applicable. Just to make sure folks are clear. queueMicrotask shouldn't be abused or is a replacement for next macrotask scheduling IE setTimeout(() => {}, 0). On review of the TRL codebase there were no reasons to change any of the macrotask scheduling to microtasks. Before you consider using queueMicrotask you should read the guide on microtasks

Did you find this page helpful?